#include "pch.h"
#include <gtest/gtest.h>
#include "FilterEngine/FilterExpression.h"
#include "FilterEngine/ILineDataProvider.h"
#include "DiffContext.h"
#include "DiffItem.h"
#include "PathContext.h"
#include "UniFile.h"
#include "Poco/DateTimeParser.h"
#include "Poco/Timezone.h"
#include "Environment.h"
#include "paths.h"
#include "OptionsMgr.h"
#include "OptionsDef.h"
#include "MergeApp.h"

#pragma execution_character_set("utf-8")

struct FilterTestParam { bool optimize; };

// Helper function to create a simple DIFFITEM for testing
static void CreateSimpleDiffItem(DIFFITEM& di)
{
	di.diffFileInfo[0].path = L"abc";
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].path = L"abc";
	di.diffFileInfo[2].path = L"abc";
	di.diffFileInfo[2].filename = L"Test.txt";
	di.diffFileInfo[2].size = 1100;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(2);
}

// The fixture for testing paths functions.
class FilterExpressionTest : public ::testing::TestWithParam<FilterTestParam> {};

TEST_P(FilterExpressionTest, Literals)
{
	// Test case for evaluating filter expressions with literal values.
	// This function sets up a test context, initializes test data, and verifies
	// the behavior of the FilterExpression class.

	// Initialize path context and diff context for the test.
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);

	// Set up DIFFITEM object with file information and timestamps.
	DIFFITEM di;
	int tzd;
	di.diffFileInfo[0].filename = L"Alice.txt";
	di.diffFileInfo[0].size = 1000;
	Poco::DateTime dt0 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M", "2025-05-16 15:34", tzd);
	dt0 -= Poco::Timezone::tzd() * 1000;
	di.diffFileInfo[0].mtime = dt0.timestamp();
	di.diffFileInfo[1].filename = L"Alice.txt";
	di.diffFileInfo[1].size = 1100;
	Poco::DateTime dt1 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:57", tzd);
	dt1 -= Poco::Timezone::tzd() * 1000;
	di.diffFileInfo[1].mtime = dt1.timestamp();

	// Set side flags for the diff code.
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	// Initialize FilterExpression and set the diff context.
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Verify that the filter expression correctly parses and evaluates literals.
	EXPECT_TRUE(fe.Parse("123 == 123"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 101 + 11 * 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 134 - 22 / 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 121 + 8 % 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 123"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 124"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(123, 124) == 124"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125, 126) == array(125, 126)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125, 126) == array(126, 125)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(125, 126), 0) == 125"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(125, 126), 1) == 126"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 < 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 < 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 2) < 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 3) < 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(2, 3) < array(3, 3)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 3) < array(3, 3)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 <= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 <= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 <= 2"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 <= array(3, 1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 <= array(3, 3)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 <= array(2, 2)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 > 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 > 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 2) > 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 3) > 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 >= 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 >= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 >= 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 2) >= 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3, 2) >= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(2, 2) >= 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 != 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 != 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125, 126) != 124"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125, 126) != array(126, 125)"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("123.4 == 123.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(".1 == 0.1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1.23 == 123e-2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1.234 == 123.4e-2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.4 == 101.4 + 11 * 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 134 - 22 / 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.4 == 121.4 + 8 % 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.0 == 123"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 123.0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.4 == 123.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.0 == 124"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123 == 124.0"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("123.4 == 124.5"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(123.4, 124.5) == 124.5"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125.6, 126.7) == array(125.6, 126.7)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125.6, 126.7) == array(126.7, 125.6)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(125.6, 126.7), 0) == 125.6"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(125.6, 126.7), 1) == 126.7"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.9 < 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 < 2.1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.8 < 2.9"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 < 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 2.3) < 3.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 3.4) < 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(2.3, 3.4) < array(3.4, 3.4)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 3.4) < array(3.4, 3.4)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.9 <= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2 <= 2.1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.9 <= 3.0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.0 <= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 <= 3.0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 <= 3.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.0 <= 2.9"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.3 <= array(3.4, 1.2)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 <= array(3.4, 3.4)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 <= array(2.3, 2.3)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.1 > 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 > 2.9"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 > 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 > 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 2.3) > 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 3.4) > 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.1 >= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 >= 2.9"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 >= 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.0 >= 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 >= 3.0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 >= 3.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2.3 >= 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 2.3) >= 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(3.4, 2.3) >= 3.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(2.3, 2.3) >= 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.0 != 3"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 != 3.0"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 != 3.4"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 != 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3 != 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("3.4 != 2.3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125.6, 126.7) != 124.5"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(125.6, 126.7) != array(126.7, 125.6)"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025-05-27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" - d\"2025-05-25\" == 2days"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025/05/27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025.05.27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025-05-28\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" < d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" < d\"2025-05-28\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" <= d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" <= d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29\" <= d\"2025-05-28\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" > d\"2025-05-27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" > d\"2025-05-28\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" >= d\"2025-05-27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" >= d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" >= d\"2025-05-29\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" != d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" != d\"2025-05-27\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(d\"2025-05-27\", d\"2025-05-28\"), 0) == d\"2025-05-27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(d\"2025-05-27\", d\"2025-05-28\"), 1) == d\"2025-05-28\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("\"abc\" == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abcde\" == \"ab\" + \"cde\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" == \"abd\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abb\" < \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abb\" < \"abb\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abb\" <= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" <= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abd\" <= \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" > \"abb\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" > \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" >= \"abb\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" >= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" >= \"abd\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" = \"abd\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" contains \"ab\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" contains \"bc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" contains \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" recontains \"a.c\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" recontains \"[a-c]\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" like \"a?c\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" like \"a*c\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" matches \"a.*c\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" matches \"^a.*\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0, 4) = \"abcd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0, 5) = \"abcd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0, 0) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0, 3) = \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 2, 2) = \"cd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(array(\"abcd\", \"efghij\"), 2, 2) = array(\"cd\", \"gh\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -1, 2) = \"d\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -2, 2) = \"cd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -5, 2) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(array(\"abcd\"), -2, 2) = array(\"cd\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0, -3) = \"a\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 1, -2) = \"b\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -3, -2) = \"b\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -3, -3) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -3, -4) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -3, -4) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 0) = \"abcd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 2) = \"cd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", 4) = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(\"abcd\", -1) = \"d\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(array(\"abcd\", \"a\"), -1) = array(\"d\", \"a\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(\"abcd\") = 4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(array(\"abcd\", \"a\")) = array(4, 1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(\"\") = 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(array(\"\")) = array(0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(array()) = array()"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"abcd\", \"ab\", \"cd\") = \"cdcd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"\", \"ab\", \"cd\") = \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"abcd\", \"cd\", \"\") = \"ab\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(array(\"abcd\"), \"cd\", \"\") = array(\"ab\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	// Case-insensitive replace: search pattern matches regardless of case
	EXPECT_TRUE(fe.Parse("replace(\"ABCD\", \"ab\", \"cd\") = \"cdCD\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"TeSt\", \"test\", \"X\") = \"X\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"Hello World\", \"hello\", \"Hi\") = \"Hi World\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(array(\"TEST\", \"TeSt\"), \"test\", \"foo\") = array(\"foo\", \"foo\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(\"ab\", \"cd\"), 0) == \"ab\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(\"ab\", \"cd\"), 1) == \"cd\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("v\"2.16.48.2\" == v\"002.016.048.002\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.48.2\" < v\"2.16.49\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.48.2\" <= v\"2.16.49\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.49\" <= v\"2.16.49\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.49\" > v\"2.16.48.2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.49\" >= v\"2.16.48.2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("v\"2.16.49\" >= v\"2.16.49\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(v\"2.16.49\", v\"2.16.50\"), 0) == v\"2.16.49\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(v\"2.16.49\", v\"2.16.50\"), 1) == v\"2.16.50\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("1weeks == 10days - 3days"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1week == 6days + 1days"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1w == 7d"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1days == 24hours"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.25days == 6hours"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1day == 24hour"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1d == 24hr"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1d == 24h"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1hours == 60minutes"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5hours == 30minutes"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1hour == 60minute"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1h == 60min"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5h == 30min"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1h == 60m"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1minutes == 60seconds"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.1minutes == 6seconds"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1minute == 60second"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1min == 60sec"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1m == 60s"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1seconds == 1000milliseconds"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1second == 1000millisecond"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1sec == 1000msec"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1s == 1000ms"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("2hours == 60minutes"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2hour == 60minute"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2h == 60min"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2h == 60m"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2minutes == 60seconds"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2minute == 60second"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2min == 60sec"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2m == 60s"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2seconds == 1000milliseconds"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2second == 1000millisecond"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2sec == 1000msec"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2s == 1000ms"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("1KB == 1024"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1kb == 1024"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5KB == 512"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1MB == 1024KB"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5MB == 512KB"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1GB == 1024MB"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5GB == 512MB"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1TB == 1024GB"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("0.5TB == 512GB"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("2KB == 1024"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2MB == 1024KB"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2GB == 1024MB"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("2TB == 1024GB"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025-05-27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025/05/27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"27-may-2025\" == d\"2025.05.27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27T00:00:00+00:00\" == d\"2025-05-27T00:00:00Z\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27T12:34:56+00:00\" == d\"2025-05-27T21:34:56+09:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"20250527T123456+0000\" == d\"20250527T213456+0900\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27T01:02:03+09:00\" != d\"2025-05-27T01:02:03Z\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" == d\"2025-05-27 00:00:01\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" < d\"2025-05-27 00:00:01\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" < d\"2025-05-27 00:00:00\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" <= d\"2025-05-27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" <= d\"2025-05-27 00:00:01\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:02\" <= d\"2025-05-27 00:00:01\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:01\" > d\"2025-05-27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:01\" > d\"2025-05-27 00:00:01\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:01\" >= d\"2025-05-27 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:01\" >= d\"2025-05-27 00:00:01\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27 00:00:01\" >= d\"2025-05-27 00:00:02\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" != d\"2025-05-27 00:00:01\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-27\" != d\"2025-05-27 00:00:00\""));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" == d\"2025-05-21\" + 1week"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" == d\"2025-05-27\" + 1day"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28 12:34:56\" == d\"2025-05-28 11:34:56\" + 1hour"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28 12:34:56\" == d\"2025-05-28 12:35:56\" - 1minute"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28 12:34:56\" == d\"2025-05-28 12:34:55\" + 1second"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-28 12:34:56\" == d\"2025-05-28 12:34:57\" - 1000millisecond"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("d\"2025-05-28\" == d\"2025-05-27 00:00:00\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29\" == d\"2025-05-21\" + 1week"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29\" == d\"2025-05-27\" + 1day"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29 12:34:56\" == d\"2025-05-28 11:34:56\" + 1hour"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29 12:34:56\" == d\"2025-05-28 12:33:56\" + 1minute"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29 12:34:56\" == d\"2025-05-28 12:34:55\" + 1second"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("d\"2025-05-29 12:34:56\" == d\"2025-05-28 12:34:55\" + 1000millisecond"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("startOfWeek(d\"2025-07-12 12:34:56\") == d\"2025-07-06 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("startOfWeek(\"2025-07-12 12:34:56\") == d\"2025-07-06 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("startOfMonth(d\"2025-07-12 12:34:56\") == d\"2025-07-01 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("startOfMonth(\"2025-07-12 12:34:56\") == d\"2025-07-01 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("startOfYear(d\"2025-07-12 12:34:56\") == d\"2025-01-01 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("startOfYear(\"2025-07-12 12:34:56\") == d\"2025-01-01 00:00:00\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toDateStr(d\"2025-07-12 12:34:56\") == \"2025-07-12\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toDateStr(array(d\"2025-07-12 12:34:56\", d\"2025-12-31 23:59:59\")) == array(\"2025-07-12\", \"2025-12-31\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toDateStr(\"2025-07-12 12:34:56\") == \"2025-07-12\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toDateStr(array(\"2025-07-12 12:34:56\", \"2025-12-31 23:59:59\")) == array(\"2025-07-12\", \"2025-12-31\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("TRUE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("FALSE"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("false"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true = TRUE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true = false"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("false = FALSE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("false = true"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(false, true), 0) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(false, true), 1) == true"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("not TRUE"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("not FALSE"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("TRUE and TRUE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("TRUE and FALSE"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("FALSE and TRUE"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("FALSE and FALSE"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("TRUE OR TRUE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("TRUE OR FALSE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("FALSE OR TRUE"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("FALSE OR FALSE"));
	EXPECT_FALSE(fe.Evaluate(di));

	// None and undefined literal tests
	EXPECT_TRUE(fe.Parse("none == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none != 123"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none != \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none != true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none != false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none != d\"2025-05-27\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none == 123"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none == \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("none == true"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(none, 123, none), 0) == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(none, 123, none), 1) == 123"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(none, 123, none), 2) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("array() = array()"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(1) = array(1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(1, 2, 3) = array(1, 2, 3)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20) = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array() = array(1)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(1) = array()"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array() = 1"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 = array()"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 = array(1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(array(1)), 0) = array(1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(array(1)), -1) = array(1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(array(1)), -2) = array(1)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("at(array(array(1)), 1) = array(1)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array())"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(123))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(123, 123, 123))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(123, 123, 124))"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(\"abc\", \"abc\", \"abc\"))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(\"abc\", \"abc\", \"abcd\"))"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(d\"2025-05-27\", d\"2025-05-27\", d\"2025-05-27\"))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(d\"2025-05-27\", d\"2025-05-27\", d\"2025-05-28\"))"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(array(1)), array(array(1)))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(array(1, 2), array(1, 2)))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(array(1, 2), array(1, 3)))"));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, FileAttributes)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src", L"E:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	int tzd;
	di.diffFileInfo[0].path = L"abc";
	di.diffFileInfo[0].filename = L"Alice.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[0].flags.attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE;
	Poco::DateTime dt0 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:56", tzd);
	dt0.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[0].mtime = dt0.timestamp();
	di.diffFileInfo[0].ctime = dt0.timestamp();
	di.diffFileInfo[0].encoding.SetCodepage(65001);
	di.diffFileInfo[0].version.SetFileVersion(0x00020010, 0x00300002);
	di.diffFileInfo[1].path = L"abc";
	di.diffFileInfo[2].path = L"abc";
	di.diffFileInfo[2].filename = L"Alice.txt";
	di.diffFileInfo[2].size = 1100;
	di.diffFileInfo[2].flags.attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE;
	Poco::DateTime dt1 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:57", tzd);
	dt1.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[2].mtime = dt1.timestamp();
	di.diffFileInfo[2].ctime = dt1.timestamp();
	di.diffFileInfo[2].encoding.SetCodepage(65001);
	di.diffFileInfo[2].version.SetFileVersion(0x00020010, 0x00300002);
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(2);
	di.nsdiffs = 3;
	di.nidiffs = 2;

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	EXPECT_TRUE(fe.Parse("Size <= 1000"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(Size)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(Size <= 1000)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("anyof(Size <= 1000)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftDate < now()"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftDate != RightDate"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftDate != MiddleDate"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftCreationTime != RightCreationTime"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftCreationTime != MiddleCreationTime"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("abs(-100) == 100"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("abs(-123.4) == 123.4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("abs(LeftSize - RightSize) == (1100 - 1000)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftSize <= 100 * (1 + 9)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftSize < 200 + 20 * 40"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleSize != 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName = \"Alice.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName = \"Bob.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName CONTAINS \"alice\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName contains \".txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName not contains \".txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName matches \"a.*t\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName not matches \"a.*t\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName like \"a?ice.*t\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName not like \"a?ice.*t\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleName != \"Alice.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftExtension = \"txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleExtension != \"txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftFolder = \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleFolder = \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftDateStr = \"2025-05-16\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleDateStr != \"2025-05-16\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("DiffCode != 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("Differences = 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("IgnoredDiffs = 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftFullPath = \"C:\\dev\\winmerge\\src\\abc\\Alice.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleFullPath != \"D:\\dev\\winmerge\\src\\abc\\Alice.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightAttributes > 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleAttributes != 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftAttrStr = \"RHSA\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleAttrStr != \"RHSA\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftCodepage = 65001"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleCodepage != 65001"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftEncoding = \"UTF-8\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleEncoding != \"UTF-8\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftVersion = v\"2.16.48.2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleVersion != v\"2.16.48.2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftExists"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightExists"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("MiddleExists"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("Not MiddleExists"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Additional file attributes tests
	EXPECT_TRUE(fe.Parse("LeftBinary == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightBinary == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftIsFolder == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightIsFolder == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("Skipped == false"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, LineAttributes)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src", L"E:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	int tzd;
	di.diffFileInfo[0].path = L"abc";
	di.diffFileInfo[0].filename = L"Alice.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[0].flags.attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE;
	Poco::DateTime dt0 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:56", tzd);
	dt0.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[0].mtime = dt0.timestamp();
	di.diffFileInfo[0].ctime = dt0.timestamp();
	di.diffFileInfo[0].encoding.SetCodepage(65001);
	di.diffFileInfo[0].version.SetFileVersion(0x00020010, 0x00300002);
	di.diffFileInfo[1].path = L"abc";
	di.diffFileInfo[2].path = L"abc";
	di.diffFileInfo[2].filename = L"Alice.txt";
	di.diffFileInfo[2].size = 1100;
	di.diffFileInfo[2].flags.attributes = FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_SYSTEM | FILE_ATTRIBUTE_ARCHIVE;
	Poco::DateTime dt1 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:57", tzd);
	dt1.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[2].mtime = dt1.timestamp();
	di.diffFileInfo[2].ctime = dt1.timestamp();
	di.diffFileInfo[2].encoding.SetCodepage(65001);
	di.diffFileInfo[2].version.SetFileVersion(0x00020010, 0x00300002);
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(2);
	di.nsdiffs = 3;
	di.nidiffs = 2;

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.diritem = false;

	struct Provider : public ILineDataProvider
	{

		std::string GetLine(int pane, int lineIndex) const override
		{
			std::string line;
			if (pane == 0)
				line = (lineIndex == 0) ? "// abc" : "// 012345";
			else if (pane == 1)
				line = (lineIndex == 0) ? "// defghij" : "// 6789";
			else
				line = (lineIndex == 0) ? "// xyz" : "// []{}-+/*";
			return line;
		}

		int GetLineCount() const override
		{
			return 2;
		}

		int GetColumnCount(int pane, int lineIndex) const override
		{
			// Return 12 columns for testing Column1 through Column10 and beyond
			return 12;
		}

		std::string GetColumn(int pane, int lineIndex, int columnIndex) const override
		{
			// Generate column data based on pane, lineIndex, and columnIndex
			if (pane == 0)
			{
				if (lineIndex == 0)
					return "Left0_Col" + std::to_string(columnIndex + 1);
				else
					return "Left1_Col" + std::to_string(columnIndex + 1);
			}
			else if (pane == 1)
			{
				if (lineIndex == 0)
					return "Middle0_Col" + std::to_string(columnIndex + 1);
				else
					return "Middle1_Col" + std::to_string(columnIndex + 1);
			}
			else
			{
				if (lineIndex == 0)
					return "Right0_Col" + std::to_string(columnIndex + 1);
				else
					return "Right1_Col" + std::to_string(columnIndex + 1);
			}
		}

		int GetRealLineNumber(int pane, int lineIndex) const override
		{
			return (pane == 0) ? (lineIndex) : (lineIndex + 10);
		}

		unsigned GetLineFlags(int pane, int lineIndex) const override
		{
			unsigned flags = 0;
			if (pane == 0 && lineIndex == 0)
				flags |= ILineDataProvider::LF_DIFF | ILineDataProvider::LF_MOVED;
			if (pane == 0 && lineIndex == 1)
				flags |= ILineDataProvider::LF_BOOKMARKS;
			if (pane == 1 && lineIndex == 0)
				flags |= ILineDataProvider::LF_GHOST;
			if (lineIndex == 2)
				flags |= ILineDataProvider::LF_TRIVIAL;
			return flags;
		}

		unsigned GetLineEol(int pane, int lineIndex) const override
		{
			if (pane == 0)
				return 1;
			else if (pane == 1)
				return 3;
			else if (pane == 2)
				return 2;
			return 0;
		}
	} provider;


	auto pFilterSharedContext = std::make_unique<FilterSharedContext>();
	FilterEvalContext ectxt{ &fe, &di, &provider, pFilterSharedContext.get() };

	// Basic Line field tests
	EXPECT_TRUE(fe.Parse("Line contains \"abc\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;
	EXPECT_FALSE(fe.Evaluate(ectxt));

	// Left/Middle/Right Line field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftLine = \"// abc\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLine = \"// defghij\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLine = \"// xyz\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftLine = \"// 012345\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLine = \"// 6789\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLine = \"// []{}-+/*\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineLength field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftLineLength = 6"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineLength = 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineLength = 6"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineNumber field tests (real line numbers)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftLineNumber = 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineNumber = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineNumber = 11"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftLineNumber = 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineNumber = 12"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// ViewLineNumber field tests (view index + 1)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("ViewLineNumber = 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("ViewLineNumber = 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineExists field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftExists"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleExists"));
	EXPECT_FALSE(fe.Evaluate(ectxt)); // Middle line 0 has LF_GHOST flag
	EXPECT_TRUE(fe.Parse("RightExists"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineMissing field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftMissing"));
	EXPECT_FALSE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleMissing"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Middle line 0 has LF_GHOST flag
	EXPECT_TRUE(fe.Parse("RightMissing"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	// LineMovedField tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftMoved"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Left line 0 has LF_MOVED flag
	EXPECT_TRUE(fe.Parse("MiddleMoved"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftMoved"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	// LineBookmarked field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftBookmarked"));
	EXPECT_FALSE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftBookmarked"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Left line 1 has LF_BOOKMARKS flag

	// LineIdentical field tests (no side prefix)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Identical"));
	EXPECT_FALSE(fe.Evaluate(ectxt)); // Left line 0 has LF_DIFF flag
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("Identical"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Left line 1 has no LF_DIFF or LF_GHOST

	// LineDifferent field tests (no side prefix)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Different"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Left line 0 has LF_DIFF flag
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("Different"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	// LineTrivial field tests (no side prefix)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Trivial"));
	EXPECT_FALSE(fe.Evaluate(ectxt)); // Left line 0 does not have LF_TRIVIAL flag

	// ColumnCount field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumnCount = 12"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnCount = 12"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnCount = 12"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Column1 field tests (no side prefix - returns array)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Column1 = array(\"Left0_Col1\", \"Middle0_Col1\", \"Right0_Col1\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("Column1 = array(\"Left1_Col1\", \"Middle1_Col1\", \"Right1_Col1\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LeftColumn1, MiddleColumn1, RightColumn1 field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn1 = \"Left0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn1 = \"Middle0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn1 = \"Right0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftColumn1 = \"Left1_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn1 = \"Middle1_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn1 = \"Right1_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Column10 field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Column10 = array(\"Left0_Col10\", \"Middle0_Col10\", \"Right0_Col10\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LeftColumn10, MiddleColumn10, RightColumn10 field tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn10 = \"Left0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn10 = \"Middle0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn10 = \"Right0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftColumn10 = \"Left1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn10 = \"Middle1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn10 = \"Right1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Column index out of range tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn13 = \"\""));
	EXPECT_FALSE(fe.Evaluate(ectxt)); // Column13 does not exist (only 12 columns)

	// Test Column with contains operator
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn1 contains \"Left0\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn5 contains \"Col5\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn10 contains \"Right0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineAt function tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LineAt(1) = array(\"// abc\", \"// defghij\", \"// xyz\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineAt(2) = array(\"// 012345\", \"// 6789\", \"// []{}-+/*\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineAt(0) = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineAt(-1) = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineAt(3) = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range returns undefined

	// LeftLineAt, MiddleLineAt, RightLineAt function tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftLineAt(1) = \"// abc\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineAt(1) = \"// defghij\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineAt(1) = \"// xyz\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftLineAt(2) = \"// 012345\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineAt(2) = \"// 6789\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineAt(2) = \"// []{}-+/*\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftLineAt(0) = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineAt(0) = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineAt(0) = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LineOffsetAt function tests (relative to current line)
	EXPECT_TRUE(fe.Parse("LineOffsetAt(0) = array(\"// abc\", \"// defghij\", \"// xyz\")"));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineOffsetAt(1) = array(\"// 012345\", \"// 6789\", \"// []{}-+/*\")"));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineOffsetAt(-1) = array(none, none, none)"));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Negative offset from line 0 is out of range
	EXPECT_TRUE(fe.Parse("LineOffsetAt(2) = array(none, none, none)"));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range

	EXPECT_TRUE(fe.Parse("LineOffsetAt(0) = array(\"// 012345\", \"// 6789\", \"// []{}-+/*\")"));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineOffsetAt(-1) = array(\"// abc\", \"// defghij\", \"// xyz\")"));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LineOffsetAt(1) = array(none, none, none)"));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range

	// LeftLineOffsetAt, MiddleLineOffsetAt, RightLineOffsetAt function tests
	EXPECT_TRUE(fe.Parse("LeftLineOffsetAt(0) = \"// abc\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineOffsetAt(0) = \"// defghij\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineOffsetAt(0) = \"// xyz\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftLineOffsetAt(1) = \"// 012345\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineOffsetAt(1) = \"// 6789\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineOffsetAt(1) = \"// []{}-+/*\""));
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Evaluate(ectxt));

	EXPECT_TRUE(fe.Parse("LeftLineOffsetAt(-1) = \"// abc\""));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleLineOffsetAt(-1) = \"// defghij\""));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightLineOffsetAt(-1) = \"// xyz\""));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Column function tests (by column name, returns array of all panes)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("Column(\"Left0_Col1\") = array(\"Left0_Col1\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("Column(\"Middle0_Col10\") = array(none, \"Middle0_Col10\", none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("Column(\"Right0_Col1\") = array(none, none, \"Right1_Col1\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("Column(\"Left0_Col5\") = array(\"Left1_Col5\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// LeftColumn, MiddleColumn, RightColumn function tests (by column name)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn(\"Left0_Col1\") = \"Left0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn(\"Middle0_Col1\") = \"Middle0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn(\"Right0_Col1\") = \"Right0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftColumn(\"Left0_Col10\") = \"Left0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumn(\"Middle0_Col10\") = \"Middle0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumn(\"Right0_Col10\") = \"Right0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// ColumnAt function tests (by row index and column name, returns array of all panes)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("ColumnAt(1, \"Left0_Col1\") = array(\"Left0_Col1\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnAt(2, \"Middle0_Col1\") = array(none, \"Middle1_Col1\", none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnAt(0, \"Left0_Col10\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnAt(-1, \"Left0_Col1\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnAt(3, \"Left0_Col1\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range

	// LeftColumnAt, MiddleColumnAt, RightColumnAt function tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumnAt(1, \"Left0_Col1\") = \"Left0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnAt(1, \"Middle0_Col1\") = \"Middle0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnAt(1, \"Right0_Col1\") = \"Right0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftColumnAt(2, \"Left0_Col10\") = \"Left1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnAt(2, \"Middle0_Col10\") = \"Middle1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnAt(2, \"Right0_Col10\") = \"Right1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftColumnAt(0, \"Left0_Col1\") = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnAt(0, \"Middle0_Col1\") = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnAt(0, \"Right0_Col1\") = none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// ColumnOffsetAt function tests (relative to current line, by column name)
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(0, \"Left0_Col1\") = array(\"Left0_Col1\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(1, \"Left0_Col1\") = array(\"Left1_Col1\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(-1, \"Left0_Col1\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Negative offset from line 0 is out of range
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(2, \"Left0_Col1\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(0, \"Left0_Col10\") = array(\"Left1_Col10\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(-1, \"Left0_Col10\") = array(\"Left0_Col10\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnOffsetAt(1, \"Left0_Col1\") = array(none, none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt)); // Out of range

	// LeftColumnOffsetAt, MiddleColumnOffsetAt, RightColumnOffsetAt function tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumnOffsetAt(0, \"Left0_Col1\") = \"Left0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnOffsetAt(0, \"Middle0_Col1\") = \"Middle0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnOffsetAt(0, \"Right0_Col1\") = \"Right0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("LeftColumnOffsetAt(1, \"Left0_Col10\") = \"Left1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnOffsetAt(1, \"Middle0_Col10\") = \"Middle1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnOffsetAt(1, \"Right0_Col10\") = \"Right1_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftColumnOffsetAt(-1, \"Left0_Col1\") = \"Left0_Col1\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleColumnOffsetAt(-1, \"Middle0_Col5\") = \"Middle0_Col5\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnOffsetAt(-1, \"Right0_Col10\") = \"Right0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Combined tests: Column functions with operators
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftColumn(\"Left0_Col1\") contains \"Left0\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("ColumnAt(2, \"Left0_Col1\") = array(\"Left1_Col1\", none, none)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("anyof(ColumnAt(1, \"Left0_Col1\") contains \"Col1\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	EXPECT_TRUE(fe.Parse("LeftLineOffsetAt(-1) contains \"abc\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightColumnOffsetAt(-1, \"Right0_Col10\") = \"Right0_Col10\""));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Additional line attributes tests
	ectxt.lineIndex = 0;
	EXPECT_TRUE(fe.Parse("LeftEOL == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("MiddleEOL == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	EXPECT_TRUE(fe.Parse("RightEOL == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
}

TEST_P(FilterExpressionTest, Content1)
{
	const String dir = paths::ConcatPath(env::GetProgPath(), L"..\\TestData");
	PathContext paths(dir, dir);
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	di.diffFileInfo[1].filename = L"LeftAndRight.WinMerge";
	di.diffFileInfo[1].path = L"";
	di.diffcode.setSideFlag(1);
	
	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	EXPECT_TRUE(fe.Parse("RightContent = RightContent"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(RightContent, RightContent))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent != LeftContent"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(Content)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent contains \"UTF-8\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftContent contains \"UTF-8\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent recontains \"xml.*UTF-8\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent not recontains \"xml.*UTF-8\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftContent recontains \"xml.*UTF-8\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("linecount(RightContent) = 7"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, 0, 1) contains \"<?xml version='1.0' encoding='UTF-8'?>\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, 1, 2) contains \"paths\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, -1, 1) contains \"</project>\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, -1, -1) contains \"</project>\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, -1, -2) contains \"</project>\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, ContentEmpty)
{
	const String dir = paths::ConcatPath(env::GetProgPath(), L"..\\..\\Data\\Compare");
	PathContext paths(paths::ConcatPath(dir, _T("dir1")), paths::ConcatPath(dir, _T("dir2")));
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	di.diffFileInfo[1].filename = L"file123_0.txt";
	di.diffFileInfo[1].path = L"";
	di.diffcode.setSideFlag(1);
	
	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	EXPECT_TRUE(fe.Parse("RightContent = RightContent"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(array(RightContent, RightContent))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent != LeftContent"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allequal(Content)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent contains \"UTF-8\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightContent recontains \"xml.*UTF-8\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("linecount(RightContent) = 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, 0, 1) contains \"<?xml version='1.0' encoding='UTF-8'?>\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, 1, 2) contains \"paths\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, -1, 1) contains \"</project>\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("sublines(RightContent, -1, -1) contains \"</project>\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, ParseError)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	EXPECT_FALSE(fe.Parse("LeftDate $ a"));
	EXPECT_EQ(FILTER_ERROR_UNKNOWN_CHAR, fe.errorCode);
	EXPECT_FALSE(fe.Parse("LeftName = \"aaa"));
	EXPECT_EQ(FILTER_ERROR_UNTERMINATED_STRING, fe.errorCode);
	EXPECT_FALSE(fe.Parse("LeftSize = 100 RightSize < 100"));
	EXPECT_EQ(FILTER_ERROR_SYNTAX_ERROR, fe.errorCode);
	EXPECT_FALSE(fe.Parse("LeftDate = d\"2025-13-32 25:60:61\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_LITERAL, fe.errorCode);
	EXPECT_FALSE(fe.Parse("aaa(1234)"));
	EXPECT_EQ(FILTER_ERROR_UNDEFINED_IDENTIFIER, fe.errorCode);
	EXPECT_FALSE(fe.Parse("aaa = 1234"));
	EXPECT_EQ(FILTER_ERROR_UNDEFINED_IDENTIFIER, fe.errorCode);
	EXPECT_FALSE(fe.Parse("abs()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("abs(1, 2)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("anyof()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("anyof(true, true)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("allof()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("allof(true, true)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("allequal()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("at()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("at(array(1,2))"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("at(array(1,2),0,1)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("strlen()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("strlen(\"abc\", \"def\")"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("substr()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("substr(\"abc\")"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("substr(\"abc\", 0, 1, 2)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("linecount()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("linecount(\"abc\", \"def\")"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("sublines()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("sublines(\"abc\")"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("sublines(\"abc\", 0, 1, 2)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("replace()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("replace(\"abc\", \"def\", \"ghi\", 2)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("today(1)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("now(1)"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfWeek()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfWeek(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfMonth()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfMonth(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfYear()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("startOfYear(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("toDateStr()"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("toDateStr(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("isWithin(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	EXPECT_FALSE(fe.Parse("inRange(now(), now())"));
	EXPECT_EQ(FILTER_ERROR_INVALID_ARGUMENT_COUNT, fe.errorCode);
	if (fe.optimize) {
		EXPECT_FALSE(fe.Parse("LeftName matches \"[[\""));
		EXPECT_EQ(FILTER_ERROR_INVALID_REGULAR_EXPRESSION, fe.errorCode);
	}
	EXPECT_FALSE(fe.Parse("abs(1)) + abc(2)"));
	EXPECT_EQ(FILTER_ERROR_SYNTAX_ERROR, fe.errorCode);
	EXPECT_FALSE(fe.Parse(")LeftSize == 1"));
	EXPECT_EQ(FILTER_ERROR_SYNTAX_ERROR, fe.errorCode);
}

TEST_P(FilterExpressionTest, IsWithinAndInRange)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Integer tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(5, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(1, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(10, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(0, 1, 10)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(11, 1, 10)"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Integer tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(5, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(1, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(10, 1, 10)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(0, 1, 10)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(11, 1, 10)"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Double tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(5.5, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(1.0, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(10.0, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(0.9, 1.0, 10.0)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(10.1, 1.0, 10.0)"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Double tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(5.5, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(1.0, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(10.0, 1.0, 10.0)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(0.9, 1.0, 10.0)"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(10.1, 1.0, 10.0)"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Mixed int/double tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(5, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(5.5, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(5, 1, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(5.5, 1.0, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Mixed int/double tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(5, 1.0, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(5.5, 1, 10)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(5, 1, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(5.5, 1, 10.0)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// String tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(\"bbb\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"aaa\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"ccc\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"ddd\", \"aaa\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"aaa\", \"bbb\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	// String tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(\"bbb\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"aaa\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"ccc\", \"aaa\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"ddd\", \"aaa\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"aaa\", \"bbb\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	// DateTime tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(d\"2025-05-15\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(d\"2025-05-10\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(d\"2025-05-20\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(d\"2025-05-09\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(d\"2025-05-21\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	// DateTime tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(d\"2025-05-15\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(d\"2025-05-10\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(d\"2025-05-20\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(d\"2025-05-09\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(d\"2025-05-21\", d\"2025-05-10\", d\"2025-05-20\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array tests for isWithin
	EXPECT_TRUE(fe.Parse("isWithin(array(3, 5, 7), 1, 10) == array(true, true, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(array(0, 5, 11), 1, 10) == array(false, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(array(1, 10), 1, 10) == array(true, true)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array tests for inRange
	EXPECT_TRUE(fe.Parse("inRange(array(3, 5, 7), 1, 10) == array(true, true, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(array(0, 5, 11), 1, 10) == array(false, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(array(1, 10), 1, 10) == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array tests with strings
	EXPECT_TRUE(fe.Parse("isWithin(array(\"aaa\", \"bbb\", \"ddd\"), \"aaa\", \"ccc\") == array(true, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(array(\"aaa\", \"bbb\", \"ccc\"), \"aaa\", \"ccc\") == array(true, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array tests with doubles
	EXPECT_TRUE(fe.Parse("isWithin(array(1.5, 5.5, 10.5), 1.0, 10.0) == array(true, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(array(1.0, 5.5, 10.0), 1.0, 10.0) == array(true, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Tests with anyof
	EXPECT_TRUE(fe.Parse("anyof(isWithin(array(0, 5, 11), 1, 10))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("anyof(inRange(array(0, 5, 11), 1, 10))"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Tests with allof
	EXPECT_TRUE(fe.Parse("allof(isWithin(array(3, 5, 7), 1, 10))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(isWithin(array(0, 5, 11), 1, 10))"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(inRange(array(3, 5, 7), 1, 10))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(inRange(array(0, 5, 11), 1, 10))"));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, StrCountAndRegexCount)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// strCount basic tests
	EXPECT_TRUE(fe.Parse("strCount(\"hello world hello\", \"hello\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"test test test\", \"test\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"abc\", \"xyz\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"aaaa\", \"aa\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di)); // Overlapping match: "aa" appears at positions 0 and 2
	EXPECT_TRUE(fe.Parse("strCount(\"\", \"test\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"test\", \"test\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));

	// strCount with arrays
	EXPECT_TRUE(fe.Parse("strCount(array(\"hello hello\", \"world\"), \"hello\") == array(2, 0)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(array(\"test test\", \"test\"), \"test\") == array(2, 1)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// strCount case-insensitive (default)
	EXPECT_TRUE(fe.Parse("strCount(\"Hello HELLO hello\", \"hello\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"Test TEST TeSt\", \"test\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));

	// strCount case-sensitive with @cs directive
	EXPECT_TRUE(fe.Parse("@cs strCount(\"Hello HELLO hello\", \"hello\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs strCount(\"Test TEST TeSt\", \"TEST\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs strCount(\"abc ABC\", \"abc\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	fe.caseSensitive = false;

	// strCount with non-string arguments (should convert)
	EXPECT_TRUE(fe.Parse("strCount(123123, \"123\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strCount(\"count: 123, value: 123\", \"123\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount basic tests
	EXPECT_TRUE(fe.Parse("regexCount(\"hello world hello\", \"hello\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"test123test456\", \"\\d+\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"abc\", \"xyz\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"AAA BBB CCC\", \"[A-Z]+\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"word1 word2 word3\", \"word\\d\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount with word boundaries
	EXPECT_TRUE(fe.Parse("regexCount(\"test testing tested\", \"\\btest\\b\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"the them there\", \"\\bthe\\b\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount with arrays
	EXPECT_TRUE(fe.Parse("regexCount(array(\"test123\", \"abc456\"), \"\\d+\") == array(1, 1)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(array(\"AAA\", \"BBB CCC\"), \"[A-Z]+\") == array(1, 2)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount case-insensitive (default)
	EXPECT_TRUE(fe.Parse("regexCount(\"Hello HELLO hello\", \"hello\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"Test TEST test\", \"test\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount case-sensitive with @cs directive
	EXPECT_TRUE(fe.Parse("@cs regexCount(\"Hello HELLO hello\", \"hello\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs regexCount(\"Test TEST test\", \"TEST\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs regexCount(\"AAA aaa\", \"[A-Z]+\") == 1"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexCount with zero-length matches (should handle gracefully)
	EXPECT_TRUE(fe.Parse("regexCount(\"abc\", \"a*\") >= 0"));
	EXPECT_TRUE(fe.Evaluate(di)); // Should not cause infinite loop

	// regexCount with non-string arguments
	EXPECT_TRUE(fe.Parse("regexCount(123456, \"\\d{3}\") == 2"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined tests with other operators
	EXPECT_TRUE(fe.Parse("strCount(\"test test test\", \"test\") > 2"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexCount(\"a1 b2 c3\", \"[a-z]\\d\") == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("anyof(strCount(array(\"hello\", \"world\"), \"hello\") > 0)"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, Test1)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	int tzd = 0;
	di.diffFileInfo[0].filename = L"Alice.txt";
	di.diffFileInfo[0].size = 1000;
	Poco::DateTime dt0 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M", "2025-05-16 15:34", tzd);
	dt0.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[0].mtime = dt0.timestamp();
	di.diffFileInfo[1].filename = L"Alice.txt";
	di.diffFileInfo[1].size = 1100;
	Poco::DateTime dt1 = Poco::DateTimeParser::parse("%Y-%m-%d %H:%M:%S", "2025-05-16 15:34:57", tzd);
	dt1.makeUTC(Poco::Timezone::tzd());
	di.diffFileInfo[1].mtime = dt1.timestamp();
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	EXPECT_TRUE(fe.Parse("1 or true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 or false"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true or 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true or 1"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 or 1"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("1 and true"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 and false"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true and 1"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("true and 1"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("1 and 1"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("not 1"));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, ConditionalFunctions)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Alice.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Bob.txt";
	di.diffFileInfo[1].size = 2000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// if function tests
	EXPECT_TRUE(fe.Parse("if(true, \"yes\", \"no\") == \"yes\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("if(false, \"yes\", \"no\") == \"no\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("if(LeftSize > 500, \"large\", \"small\") == \"large\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("if(LeftSize < 500, \"large\", \"small\") == \"small\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// ifEach function tests
	EXPECT_TRUE(fe.Parse("ifEach(array(true, false, true), \"yes\", \"no\") == array(\"yes\", \"no\", \"yes\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("ifEach(Size > 1500, \"large\", \"small\") == array(\"small\", \"large\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("ifEach(array(true, false), array(\"a\", \"b\"), array(\"c\", \"d\")) == array(\"a\", \"d\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("ifEach(true, \"yes\", \"no\") == \"yes\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// choose function tests
	EXPECT_TRUE(fe.Parse("choose(0, \"zero\", \"one\", \"two\") == \"zero\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("choose(1, \"zero\", \"one\", \"two\") == \"one\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("choose(2, \"zero\", \"one\", \"two\") == \"two\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("choose(-1, \"zero\", \"one\", \"two\") == \"zero\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("choose(5, \"zero\", \"one\", \"two\") == \"two\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// chooseEach function tests
	EXPECT_TRUE(fe.Parse("chooseEach(array(0, 1, 2), \"a\", \"b\", \"c\") == array(\"a\", \"b\", \"c\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("chooseEach(array(0, 0, 1), \"x\", \"y\", \"z\") == array(\"x\", \"x\", \"y\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("chooseEach(array(-1, 5), \"a\", \"b\") == array(\"a\", \"b\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("chooseEach(0, \"a\", \"b\") == \"a\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// andEach function tests
	EXPECT_TRUE(fe.Parse("andEach(true, true) == true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(true, false) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(false, true) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(false, false) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(array(true, false, true), true) == array(true, false, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(true, array(true, false, true)) == array(true, false, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(array(true, false, true), array(true, true, false)) == array(true, false, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("andEach(Size > 500, Size < 1500) == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// orEach function tests
	EXPECT_TRUE(fe.Parse("orEach(true, true) == true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(true, false) == true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(false, true) == true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(false, false) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(array(true, false, true), false) == array(true, false, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(false, array(true, false, true)) == array(true, false, true)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(array(true, false, false), array(false, true, false)) == array(true, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("orEach(Size < 500, Size > 1500) == array(false, true)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// notEach function tests
	EXPECT_TRUE(fe.Parse("notEach(true) == false"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("notEach(false) == true"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("notEach(array(true, false, true)) == array(false, true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("notEach(Size > 1500) == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined tests
	EXPECT_TRUE(fe.Parse("anyof(andEach(Size > 500, Size < 1500))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(orEach(Size < 500, Size > 1500))"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("anyof(notEach(Size > 2500))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("ifEach(andEach(Size > 500, Size < 1500), \"in range\", \"out of range\") == array(\"in range\", \"out of range\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, StringTransformFunctions)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// tolower function tests
	EXPECT_TRUE(fe.Parse("tolower(\"HELLO\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("tolower(\"Hello World\") == \"hello world\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("tolower(\"ABC123\") == \"abc123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("tolower(array(\"HELLO\", \"WORLD\")) == array(\"hello\", \"world\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toupper function tests
	EXPECT_TRUE(fe.Parse("toupper(\"hello\") == \"HELLO\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toupper(\"Hello World\") == \"HELLO WORLD\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toupper(\"abc123\") == \"ABC123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toupper(array(\"hello\", \"world\")) == array(\"HELLO\", \"WORLD\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Japanese character conversion tests (tohalfwidth/tofullwidth)
	EXPECT_TRUE(fe.Parse(u8"tohalfwidth(\"ＡＢＣＤＥ\") == \"ABCDE\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tofullwidth(\"ABCDE\") == \"ＡＢＣＤＥ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohalfwidth(\"１２３４５\") == \"12345\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tofullwidth(\"12345\") == \"１２３４５\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohalfwidth(array(\"ＡＢＣＤＥ\", \"１２３４５\")) == array(\"ABCDE\", \"12345\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tofullwidth(array(\"ABCDE\", \"12345\")) == array(\"ＡＢＣＤＥ\", \"１２３４５\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Japanese hiragana/katakana conversion tests
	EXPECT_TRUE(fe.Parse(u8"tokatakana(\"あいうえお\") == \"アイウエオ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohiragana(\"アイウエオ\") == \"あいうえお\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tokatakana(\"かきくけこ\") == \"カキクケコ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohiragana(\"カキクケコ\") == \"かきくけこ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tokatakana(array(\"あいうえお\", \"かきくけこ\")) == array(\"アイウエオ\", \"カキクケコ\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohiragana(array(\"アイウエオ\", \"カキクケコ\")) == array(\"あいうえお\", \"かきくけこ\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Chinese character conversion tests (simplified/traditional)
	EXPECT_TRUE(fe.Parse(u8"tosimplifiedchinese(\"繁體字\") == \"繁体字\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"totraditionalchinese(\"简体字\") == \"簡體字\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tosimplifiedchinese(\"電腦\") == \"电脑\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"totraditionalchinese(\"计算机\") == \"計算機\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tosimplifiedchinese(array(\"繁體字\", \"電腦\")) == array(\"繁体字\", \"电脑\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"totraditionalchinese(array(\"简体字\", \"计算机\")) == array(\"簡體字\", \"計算機\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined usage tests
	EXPECT_TRUE(fe.Parse(u8"toupper(tolower(\"HeLLo\")) == \"HELLO\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohalfwidth(tofullwidth(\"ABC\")) == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tokatakana(tohiragana(\"アイウエオ\")) == \"アイウエオ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"totraditionalchinese(tosimplifiedchinese(\"繁體字\")) == \"繁體字\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Edge case: empty string
	EXPECT_TRUE(fe.Parse("tolower(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toupper(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("tohalfwidth(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("tofullwidth(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Mixed content tests
	EXPECT_TRUE(fe.Parse("tolower(\"Test123!@#\") == \"test123!@#\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toupper(\"Test123!@#\") == \"TEST123!@#\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tohalfwidth(\"Ａ１あア\") == \"A1あｱ\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"tokatakana(\"あア\") == \"アア\""));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, StringFunctionsWithNonStringArguments)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// strlen with non-string arguments
	EXPECT_TRUE(fe.Parse("strlen(123) == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(12345) == 5"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(123.45) == 10")); // "123.450000"
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(true) == 4"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(false) == 5"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(array(123, 456)) == array(3, 3)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// substr with non-string first argument
	EXPECT_TRUE(fe.Parse("substr(12345, 1, 3) == \"234\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(123.45, 0, 3) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(true, 0, 2) == \"tr\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("substr(array(123, 456), 1, 2) == array(\"23\", \"56\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// replace with non-string first argument
	EXPECT_TRUE(fe.Parse("replace(12345, \"23\", \"XX\") == \"1XX45\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(123.45, \".\", \",\") == \"123,450000\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(true, \"t\", \"T\") == \"True\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(array(123, 456), \"3\", \"X\") == array(\"12X\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexReplace with non-string first argument
	EXPECT_TRUE(fe.Parse("regexReplace(12345, \"[24]\", \"X\") == \"1X3X5\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(123.45, \"\\d\", \"X\") == \"XXX.XXXXXX\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(true, \"[a-z]\", \"X\") == \"XXXX\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(array(123, 456), \"[13]\", \"X\") == array(\"X2X\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toUpper with non-string argument
	EXPECT_TRUE(fe.Parse("toUpper(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toUpper(123.45) == \"123.450000\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toUpper(true) == \"TRUE\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toUpper(false) == \"FALSE\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toUpper(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toLower with non-string argument
	EXPECT_TRUE(fe.Parse("toLower(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toLower(123.45) == \"123.450000\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toLower(true) == \"true\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toLower(false) == \"false\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toLower(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toHalfWidth with non-string argument
	EXPECT_TRUE(fe.Parse("toHalfWidth(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toHalfWidth(true) == \"true\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toHalfWidth(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toFullWidth with non-string argument
	EXPECT_TRUE(fe.Parse("toFullWidth(123) == \"１２３\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse(u8"toFullWidth(array(123, 456)) == array(\"１２３\", \"４５６\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toHiragana with non-string argument
	EXPECT_TRUE(fe.Parse("toHiragana(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toHiragana(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toKatakana with non-string argument
	EXPECT_TRUE(fe.Parse("toKatakana(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toKatakana(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toSimplifiedChinese with non-string argument
	EXPECT_TRUE(fe.Parse("toSimplifiedChinese(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toSimplifiedChinese(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// toTraditionalChinese with non-string argument
	EXPECT_TRUE(fe.Parse("toTraditionalChinese(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toTraditionalChinese(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// normalizeUnicode with non-string argument
	EXPECT_TRUE(fe.Parse("normalizeUnicode(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("normalizeUnicode(true) == \"true\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("normalizeUnicode(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, RegexExtract)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Basic regex capture tests - capturing entire match (group 0, default)
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"[a-z]+\\d+\") == \"abc123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test-456\", \"[a-z]+-\\d+\") == \"test-456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"hello world\", \"\\w+\\s\\w+\") == \"hello world\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Explicit group 0 (same as default)
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"[a-z]+\\d+\", 0) == \"abc123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test-456\", \"[a-z]+-\\d+\", 0) == \"test-456\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Capture group 1
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 1) == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test-456\", \"([a-z]+)-(\\d+)\", 1) == \"test\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"hello world\", \"(\\w+)\\s(\\w+)\", 1) == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Capture group 2
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 2) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test-456\", \"([a-z]+)-(\\d+)\", 2) == \"456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"hello world\", \"(\\w+)\\s(\\w+)\", 2) == \"world\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Capture group 3
	EXPECT_TRUE(fe.Parse("regexExtract(\"2025-05-16\", \"(\\d{4})-(\\d{2})-(\\d{2})\", 3) == \"16\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"2025-05-16\", \"(\\d{4})-(\\d{2})-(\\d{2})\", 2) == \"05\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"2025-05-16\", \"(\\d{4})-(\\d{2})-(\\d{2})\", 1) == \"2025\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// No match returns undefined
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"xyz\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test\", \"\\d+\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"1234\", \"[A-Z]+\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Out of range group returns undefined
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 3) == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"test\", \"([a-z]+)\", 2) == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc\", \"([a-z])\", 5) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Negative group index (should fail or return undefined)
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", -1) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Empty string
	EXPECT_TRUE(fe.Parse("regexExtract(\"\", \"[a-z]+\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array of strings - default group 0
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"abc123\", \"def456\"), \"[a-z]+\\d+\") == array(\"abc123\", \"def456\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"test\", \"hello123\"), \"[a-z]+\\d+\") == array(none, \"hello123\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array of strings - group 1
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"abc123\", \"def456\"), \"([a-z]+)(\\d+)\", 1) == array(\"abc\", \"def\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"test-1\", \"hello-2\"), \"([a-z]+)-(\\d)\", 1) == array(\"test\", \"hello\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array of strings - group 2
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"abc123\", \"def456\"), \"([a-z]+)(\\d+)\", 2) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array with some non-matching strings
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"abc123\", \"xyz\", \"def456\"), \"([a-z]+)(\\d+)\", 1) == array(\"abc\", none, \"def\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"test\", \"123\", \"hello456\"), \"([a-z]+)(\\d+)\", 2) == array(none, none, \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Empty array
	EXPECT_TRUE(fe.Parse("regexExtract(array(), \"[a-z]+\") == array()"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-insensitive matching (default)
	EXPECT_TRUE(fe.Parse("regexExtract(\"ABC123\", \"([a-z]+)(\\d+)\", 1) == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"Test\", \"([a-z]+)\", 1) == \"Test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-sensitive matching with @cs directive
	EXPECT_TRUE(fe.Parse("@cs regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 1) == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs regexExtract(\"ABC123\", \"([a-z]+)(\\d+)\", 1) == none"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("@cs regexExtract(\"ABC123\", \"([A-Z]+)(\\d+)\", 1) == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Real-world use cases
	EXPECT_TRUE(fe.Parse("regexExtract(\"file-v1.2.3.txt\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 1) == \"1\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"file-v1.2.3.txt\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 2) == \"2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"file-v1.2.3.txt\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 3) == \"3\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"error: line 123\", \"line\\s+(\\d+)\", 1) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"user@example.com\", \"([^@]+)@([^@]+)\", 1) == \"user\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"user@example.com\", \"([^@]+)@([^@]+)\", 2) == \"example.com\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined with other functions
	EXPECT_TRUE(fe.Parse("toUpper(regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 1)) == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("strlen(regexExtract(\"test-456\", \"([a-z]+)-(\\d+)\", 2)) == 3"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 1) + regexExtract(\"abc123\", \"([a-z]+)(\\d+)\", 2) == \"abc123\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Non-string arguments (should convert to string)
	EXPECT_TRUE(fe.Parse("regexExtract(123456, \"(\\d{3})(\\d{3})\", 1) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(123456, \"(\\d{3})(\\d{3})\", 2) == \"456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(true, \"(\\w+)\", 1) == \"true\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Offset parameter tests - capturing Nth occurrence
	// String "value 123 and value 456": two occurrences of "value \\d+"
	EXPECT_TRUE(fe.Parse("regexExtract(\"value 123 and value 456\", \"value\\s+(\\d+)\", 0, 0) == \"value 123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"value 123 and value 456\", \"value\\s+(\\d+)\", 0, 1) == \"value 456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"value 123 and value 456\", \"value\\s+(\\d+)\", 1, 0) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"value 123 and value 456\", \"value\\s+(\\d+)\", 1, 1) == \"456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"value 123 and value 456\", \"value\\s+(\\d+)\", 1, 2) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Multiple numbers in a line - capture second number
	EXPECT_TRUE(fe.Parse("regexExtract(\"error 10 warning 20 error 30\", \"(\\d+)\", 0, 0) == \"10\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"error 10 warning 20 error 30\", \"(\\d+)\", 0, 1) == \"20\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"error 10 warning 20 error 30\", \"(\\d+)\", 0, 2) == \"30\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"error 10 warning 20 error 30\", \"(\\d+)\", 1, 1) == \"20\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Real-world: extract second version number from text
	EXPECT_TRUE(fe.Parse("regexExtract(\"v1.0.0 and v2.0.0\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 1, 0) == \"1\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"v1.0.0 and v2.0.0\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 1, 1) == \"2\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"v1.0.0 and v2.0.0\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 0, 0) == \"v1.0.0\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(\"v1.0.0 and v2.0.0\", \"v(\\d+)\\.(\\d+)\\.(\\d+)\", 0, 1) == \"v2.0.0\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array with offset
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"num 1 num 2\", \"num 3 num 4\"), \"num\\s+(\\d+)\", 1, 1) == array(\"2\", \"4\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexExtract(array(\"num 1 num 2\", \"num 3 num 4\"), \"num\\s+(\\d+)\", 1, 0) == array(\"1\", \"3\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, TrimFunctions)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// trim function tests - removes leading and trailing whitespace
	EXPECT_TRUE(fe.Parse("trim(\"  hello  \") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"hello\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"  hello\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"hello  \") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"   \") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"\t\nhello\r\n\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(\"  hello world  \") == \"hello world\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// trim with array
	EXPECT_TRUE(fe.Parse("trim(array(\"  hello  \", \"  world  \")) == array(\"hello\", \"world\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(array(\"  test\", \"data  \")) == array(\"test\", \"data\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// trimLeft function tests - removes leading whitespace only
	EXPECT_TRUE(fe.Parse("trimLeft(\"  hello  \") == \"hello  \""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"hello\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"  hello\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"hello  \") == \"hello  \""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"   \") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(\"\t\nhello\r\n\") == \"hello\r\n\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// trimLeft with array
	EXPECT_TRUE(fe.Parse("trimLeft(array(\"  hello  \", \"  world  \")) == array(\"hello  \", \"world  \")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// trimRight function tests - removes trailing whitespace only
	EXPECT_TRUE(fe.Parse("trimRight(\"  hello  \") == \"  hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"hello\") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"  hello\") == \"  hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"hello  \") == \"hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"   \") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(\"\t\nhello\r\n\") == \"\t\nhello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// trimRight with array
	EXPECT_TRUE(fe.Parse("trimRight(array(\"  hello  \", \"  world  \")) == array(\"  hello\", \"  world\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// trim functions with non-string arguments
	EXPECT_TRUE(fe.Parse("trim(123) == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(true) == \"true\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimLeft(456) == \"456\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trimRight(789) == \"789\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(array(123, 456)) == array(\"123\", \"456\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined usage
	EXPECT_TRUE(fe.Parse("trim(\"  hello  \") == trimLeft(trimRight(\"  hello  \"))"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("toUpper(trim(\"  hello  \")) == \"HELLO\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("trim(replace(\"  hello  world  \", \"world\", \"test\")) == \"hello  test\""));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, ReplaceWithList)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Create temporary directory path
	const String tempDir = env::GetTemporaryPath();
	const String replaceListPath = paths::ConcatPath(tempDir, L"test_replacelist.txt");
	const String regexReplaceListPath = paths::ConcatPath(tempDir, L"test_regex_replacelist.txt");

	// Create replace list file
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(replaceListPath));
		file.WriteBom();
		file.WriteString(L"# Comment line\n");
		file.WriteString(L"apple\tAPL\n");
		file.WriteString(L"orange\tORG\n");
		file.WriteString(L"banana\tBAN\n");
		file.WriteString(L"# Another comment\n");
		file.WriteString(L"grape\tGRP\n");
		file.Close();
	}

	// Create regex replace list file
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(regexReplaceListPath));
		file.WriteBom();
		file.WriteString(L"# Regex patterns\n");
		file.WriteString(L"\\d+\t----\n");
		file.WriteString(L"[a-z]+\t****\n");
		file.WriteString(L"\\s+\t|\n");
		file.Close();
	}

	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	// Test replaceWithList function
	EXPECT_TRUE(fe.Parse("replaceWithList(\"I like apple and orange\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"I like APL and ORG\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"banana is yellow\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"BAN is yellow\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"grape juice\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"GRP juice\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Multiple replacements in one string
	EXPECT_TRUE(fe.Parse("replaceWithList(\"apple orange banana\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"APL ORG BAN\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// No match
	EXPECT_TRUE(fe.Parse("replaceWithList(\"watermelon\", \"" + ucr::toUTF8(replaceListPath) + u8"\") == \"watermelon\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Empty string
	EXPECT_TRUE(fe.Parse("replaceWithList(\"\", \"" + ucr::toUTF8(replaceListPath) + u8"\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array of strings
	EXPECT_TRUE(fe.Parse("replaceWithList(array(\"apple\", \"banana\"), \"" + ucr::toUTF8(replaceListPath) + "\") == array(\"APL\", \"BAN\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-insensitive: uppercase input matches lowercase pattern
	EXPECT_TRUE(fe.Parse("replaceWithList(\"APPLE\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"APL\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-insensitive: mixed case input
	EXPECT_TRUE(fe.Parse("replaceWithList(\"Apple is good\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"APL is good\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-insensitive: multiple mixed-case words
	EXPECT_TRUE(fe.Parse("replaceWithList(\"ORANGE and Banana\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"ORG and BAN\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Case-insensitive: array with mixed case
	EXPECT_TRUE(fe.Parse("replaceWithList(array(\"APPLE\", \"GRAPE\"), \"" + ucr::toUTF8(replaceListPath) + "\") == array(\"APL\", \"GRP\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test regexReplaceWithList function
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"abc123def456\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"****----****----\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"Hello World 2025\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"****|****|----\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"test 123\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"****|----\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Multiple spaces
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"a   b\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"****|****\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// No match
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"!@#$%\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"!@#$%\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Empty string
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array of strings
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(array(\"abc123\", \"789XYZ789\"), \"" + ucr::toUTF8(regexReplaceListPath) + "\") == array(\"****----\", \"----****----\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test with non-existent file (should return original string)
	EXPECT_TRUE(fe.Parse("replaceWithList(\"test\", \"nonexistent.txt\") == \"test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"test\", \"nonexistent.txt\") == \"test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Cleanup temporary files
	DeleteFile(replaceListPath.c_str());
	DeleteFile(regexReplaceListPath.c_str());
}

TEST_P(FilterExpressionTest, ReplaceWithListAdvanced)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	const String tempDir = env::GetTemporaryPath();
	const String replaceListPath = paths::ConcatPath(tempDir, L"test_replacelist2.txt");
	const String regexReplaceListPath = paths::ConcatPath(tempDir, L"test_regex_replacelist2.txt");

	// Create replace list with edge cases
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(replaceListPath));
		file.WriteBom();
		file.WriteString(L"# Test special characters\n");
		file.WriteString(L"C++\tCPlusPlus\n");
		file.WriteString(L"a\tb\tc\td\n"); // Extra tabs should be ignored
		file.WriteString(L"\n"); // Empty line should be skipped
		file.WriteString(L"test\t\n"); // Replace with empty string
		file.WriteString(L"# Comment at end\n");
		file.Close();
	}

	// Create regex replace list with capturing groups
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(regexReplaceListPath));
		file.WriteBom();
		file.WriteString(L"# Regex with captures\n");
		file.WriteString(L"(\\d{4})-(\\d{2})-(\\d{2})\t$3/$2/$1\n");
		file.WriteString(L"\\b(\\w+)\\s+\\1\\b\t$1\n"); // Remove duplicate words
		file.Close();
	}

	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	// Test special characters
	EXPECT_TRUE(fe.Parse("replaceWithList(\"I love C++\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"I love CPlusPlus\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test extra tabs (should only use first two fields)
	EXPECT_TRUE(fe.Parse("replaceWithList(\"a\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"b\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test replace with empty string
	EXPECT_TRUE(fe.Parse("replaceWithList(\"test123\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"123\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test regex with date format
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"2025-05-16\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"16/05/2025\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Test regex for removing duplicate words
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"the the cat\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"the cat\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexReplaceWithList(\"hello hello world world\", \"" + ucr::toUTF8(regexReplaceListPath) + "\") == \"hello world\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Combined test with array
	EXPECT_TRUE(fe.Parse("regexReplaceWithList(array(\"2025-05-16\", \"2025-12-31\"), \"" + ucr::toUTF8(regexReplaceListPath) + "\") == array(\"16/05/2025\", \"31/12/2025\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Cleanup
	DeleteFile(replaceListPath.c_str());
	DeleteFile(regexReplaceListPath.c_str());
}

TEST_P(FilterExpressionTest, ReplaceWithListEncoding)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	const String tempDir = env::GetTemporaryPath();
	const String replaceListPath = paths::ConcatPath(tempDir, L"test_replacelist_utf8.txt");

	// Create UTF-8 encoded file with word replacements
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(replaceListPath));
		file.WriteBom();
		file.WriteString(L"# UTF-8 encoding test\n");
		file.WriteString(L"hello\tgreetings\n");
		file.WriteString(L"goodbye\tfarewell\n");
		file.WriteString(L"thanks\tgratitude\n");
		file.WriteString(L"dog\tcanine\n");
		file.WriteString(L"cat\tfeline\n");
		file.Close();
	}

	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	// Test word replacement
	EXPECT_TRUE(fe.Parse("replaceWithList(\"hello\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"greetings\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"goodbye\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"farewell\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"my dog and cat\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"my canine and feline\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array test
	EXPECT_TRUE(fe.Parse("replaceWithList(array(\"hello\", \"thanks\"), \"" + ucr::toUTF8(replaceListPath) + "\") == array(\"greetings\", \"gratitude\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Cleanup
	DeleteFile(replaceListPath.c_str());
}

TEST_P(FilterExpressionTest, CaseSensitiveStringComparison)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// String comparison with case sensitivity
	EXPECT_TRUE(fe.Parse("\"abc\" == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" == \"ABC\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"ABC\" == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" != \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" != \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Less than / greater than with case sensitivity
	EXPECT_TRUE(fe.Parse("\"abc\" < \"abd\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"ABC\" < \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di)); // Uppercase comes before lowercase in ASCII
	EXPECT_TRUE(fe.Parse("\"abc\" > \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" <= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" <= \"ABC\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"ABC\" <= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" >= \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc\" >= \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"ABC\" >= \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array comparisons
	EXPECT_TRUE(fe.Parse("array(\"abc\", \"def\") == array(\"abc\", \"def\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"abc\", \"def\") == array(\"ABC\", \"DEF\")"));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"abc\", \"def\") != array(\"ABC\", \"DEF\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveContains)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// contains operator with case sensitivity
	EXPECT_TRUE(fe.Parse("\"Hello World\" contains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" contains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" contains \"World\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" contains \"world\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" contains \"HELLO\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array with contains
	EXPECT_TRUE(fe.Parse("array(\"Hello\", \"WORLD\") contains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"Hello\", \"WORLD\") contains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"Hello\", \"WORLD\") contains \"WORLD\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"Hello\", \"WORLD\") contains \"world\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveRegex)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// recontains with case sensitivity
	EXPECT_TRUE(fe.Parse("\"Hello World\" recontains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" recontains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" recontains \"[A-Z][a-z]+\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" recontains \"^hello\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello World\" recontains \"^Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// matches with case sensitivity
	EXPECT_TRUE(fe.Parse("\"Hello\" matches \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello\" matches \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello\" matches \"H.*o\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello\" matches \"h.*o\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello\" matches \"[A-Z][a-z]+\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"hello\" matches \"[A-Z][a-z]+\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array with regex
	EXPECT_TRUE(fe.Parse("array(\"Hello\", \"World\") matches \"^[A-Z][a-z]+\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("array(\"hello\", \"world\") matches \"^[A-Z][a-z]+\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveLike)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// like operator with case sensitivity
	EXPECT_TRUE(fe.Parse("\"Hello.txt\" like \"Hello.*\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Hello.txt\" like \"hello.*\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Test.TXT\" like \"*.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"Test.TXT\" like \"*.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"ABC.doc\" like \"[A-Z]*.doc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("\"abc.doc\" like \"[A-Z]*.doc\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array with like
	EXPECT_TRUE(fe.Parse("array(\"Test.TXT\", \"Test.txt\") like \"*.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("allof(array(\"Test.TXT\", \"Test.txt\") like \"*.TXT\")"));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveReplace)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// replace function with case sensitivity
	EXPECT_TRUE(fe.Parse("replace(\"Hello World\", \"Hello\", \"Hi\") == \"Hi World\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"Hello World\", \"hello\", \"Hi\") == \"Hello World\""));
	EXPECT_TRUE(fe.Evaluate(di)); // No replacement because case doesn't match
	EXPECT_TRUE(fe.Parse("replace(\"HELLO WORLD\", \"HELLO\", \"Hi\") == \"Hi WORLD\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"HELLO WORLD\", \"hello\", \"Hi\") == \"HELLO WORLD\""));
	EXPECT_TRUE(fe.Evaluate(di)); // No replacement
	EXPECT_TRUE(fe.Parse("replace(\"Test Test test\", \"Test\", \"X\") == \"X X test\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(\"Test Test test\", \"test\", \"X\") == \"Test Test X\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array with replace
	EXPECT_TRUE(fe.Parse("replace(array(\"Hello\", \"HELLO\"), \"Hello\", \"Hi\") == array(\"Hi\", \"HELLO\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("replace(array(\"Test\", \"test\"), \"Test\", \"X\") == array(\"X\", \"test\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveRegexReplace)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// regexReplace with case sensitivity
	EXPECT_TRUE(fe.Parse("regexReplace(\"Hello World\", \"Hello\", \"Hi\") == \"Hi World\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(\"Hello World\", \"hello\", \"Hi\") == \"Hello World\""));
	EXPECT_TRUE(fe.Evaluate(di)); // No replacement
	EXPECT_TRUE(fe.Parse("regexReplace(\"ABC abc\", \"[A-Z]+\", \"X\") == \"X abc\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(\"ABC abc\", \"[a-z]+\", \"X\") == \"ABC X\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(\"Test TEST test\", \"Test\", \"X\") == \"X TEST test\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(\"Test TEST test\", \"TEST\", \"X\") == \"Test X test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array with regexReplace
	EXPECT_TRUE(fe.Parse("regexReplace(array(\"Hello\", \"HELLO\"), \"Hello\", \"Hi\") == array(\"Hi\", \"HELLO\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("regexReplace(array(\"ABC\", \"abc\"), \"[A-Z]+\", \"X\") == array(\"X\", \"abc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveIsWithinAndInRange)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// isWithin with case-sensitive string comparison
	EXPECT_TRUE(fe.Parse("isWithin(\"abc\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"ABC\", \"AAA\", \"ZZZ\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"abc\", \"AAA\", \"ZZZ\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Lowercase 'a' > uppercase 'Z' in ASCII
	EXPECT_TRUE(fe.Parse("isWithin(\"ABC\", \"aaa\", \"zzz\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Uppercase 'A' < lowercase 'a'
	EXPECT_TRUE(fe.Parse("isWithin(\"bbb\", \"aaa\", \"ccc\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("isWithin(\"BBB\", \"aaa\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // 'B' < 'a' in ASCII

	// inRange with case-sensitive string comparison
	EXPECT_TRUE(fe.Parse("inRange(\"abc\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"ABC\", \"AAA\", \"ZZZ\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"aaa\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(\"zzz\", \"aaa\", \"zzz\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Exclusive upper bound
	EXPECT_TRUE(fe.Parse("inRange(\"BBB\", \"aaa\", \"ccc\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // 'B' < 'a'

	// Array tests
	EXPECT_TRUE(fe.Parse("isWithin(array(\"abc\", \"ABC\"), \"aaa\", \"zzz\") == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("inRange(array(\"abc\", \"ABC\"), \"aaa\", \"zzz\") == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, CaseSensitiveReplaceWithList)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	const String tempDir = env::GetTemporaryPath();
	const String replaceListPath = paths::ConcatPath(tempDir, L"test_replacelist_casesensitive.txt");

	// Create replace list file
	{
		UniStdioFile file;
		EXPECT_TRUE(file.OpenCreateUtf8(replaceListPath));
		file.WriteBom();
		file.WriteString(L"# Case-sensitive replacements\n");
		file.WriteString(L"Hello\tHI\n");
		file.WriteString(L"World\tEARTH\n");
		file.WriteString(L"test\tEXAM\n");
		file.Close();
	}

	GetOptionsMgr()->InitOption(OPT_CP_DETECT, 0);

	// Case-sensitive: exact match required
	EXPECT_TRUE(fe.Parse("replaceWithList(\"Hello World\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"HI EARTH\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// No match due to case difference
	EXPECT_TRUE(fe.Parse("replaceWithList(\"hello world\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"hello world\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"HELLO WORLD\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"HELLO WORLD\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Partial match
	EXPECT_TRUE(fe.Parse("replaceWithList(\"test Test\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"EXAM Test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("replaceWithList(\"Test test\", \"" + ucr::toUTF8(replaceListPath) + "\") == \"Test EXAM\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array test
	EXPECT_TRUE(fe.Parse("replaceWithList(array(\"Hello\", \"hello\"), \"" + ucr::toUTF8(replaceListPath) + "\") == array(\"HI\", \"hello\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Cleanup
	DeleteFile(replaceListPath.c_str());
}

TEST_P(FilterExpressionTest, CaseSensitiveFileAttributes)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].path = L"abc";
	di.diffFileInfo[0].filename = L"Test.TXT";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].path = L"abc";
	di.diffFileInfo[1].filename = L"test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.caseSensitive = true;

	// File name comparison with case sensitivity
	EXPECT_TRUE(fe.Parse("LeftName == \"Test.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName == \"test.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName == \"test.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName == \"Test.TXT\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// File name with contains
	EXPECT_TRUE(fe.Parse("LeftName contains \"Test\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName contains \"test\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName contains \".TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName contains \".txt\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// File name with matches
	EXPECT_TRUE(fe.Parse("LeftName matches \"Test\\.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftName matches \"test\\.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightName matches \"test\\.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Extension comparison
	di.diffFileInfo[0].filename = L"file.TXT";
	di.diffFileInfo[1].filename = L"file.txt";
	EXPECT_TRUE(fe.Parse("LeftExtension == \"TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("LeftExtension == \"txt\""));
	EXPECT_FALSE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightExtension == \"txt\""));
	EXPECT_TRUE(fe.Evaluate(di));
	EXPECT_TRUE(fe.Parse("RightExtension == \"TXT\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveCaseSensitive)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Default: case-insensitive
	EXPECT_TRUE(fe.Parse("\"ABC\" == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// @cs directive: case-sensitive
	EXPECT_TRUE(fe.Parse("@cs \"ABC\" == \"abc\""));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs \"ABC\" == \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// @casesensitive directive
	EXPECT_TRUE(fe.Parse("@casesensitive \"Test\" == \"test\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveCaseInsensitive)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// @ci directive: case-insensitive (explicit)
	EXPECT_TRUE(fe.Parse("@ci \"ABC\" == \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// @caseinsensitive directive
	EXPECT_TRUE(fe.Parse("@caseinsensitive \"Test\" == \"test\""));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveWithOperators)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// contains with @cs
	EXPECT_TRUE(fe.Parse("@cs \"Hello World\" contains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs \"Hello World\" contains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// like with @cs
	EXPECT_TRUE(fe.Parse("@cs \"Test.TXT\" like \"*.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs \"Test.TXT\" like \"*.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// matches with @cs
	EXPECT_TRUE(fe.Parse("@cs \"Hello\" matches \"H.*o\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs \"Hello\" matches \"h.*o\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// recontains with @cs
	EXPECT_TRUE(fe.Parse("@cs \"Hello World\" recontains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs \"Hello World\" recontains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// String comparison with @cs
	EXPECT_TRUE(fe.Parse("@cs \"ABC\" < \"abc\""));
	EXPECT_TRUE(fe.Evaluate(di)); // Uppercase comes before lowercase in ASCII

	EXPECT_TRUE(fe.Parse("@cs \"abc\" > \"ABC\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// replace with @cs
	EXPECT_TRUE(fe.Parse("@cs replace(\"Hello World\", \"Hello\", \"Hi\") == \"Hi World\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs replace(\"Hello World\", \"hello\", \"Hi\") == \"Hello World\""));
	EXPECT_TRUE(fe.Evaluate(di)); // No replacement
}

TEST_P(FilterExpressionTest, DirectiveInvalidSyntax)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Invalid directive
	EXPECT_FALSE(fe.Parse("@unknown \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@xyz \"Test\" == \"test\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	// Typo in directive
	EXPECT_FALSE(fe.Parse("@casesensitve \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	// Flag directives should not accept values
	EXPECT_FALSE(fe.Parse("@cs=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@cs=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@casesensitive=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@casesensitive=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@ci=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@ci=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@caseinsensitive=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@caseinsensitive=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@optimize=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@optimize=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@opt=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@nooptimize=true \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@noopt=false \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	// Invalid values for any directive
	EXPECT_FALSE(fe.Parse("@cs=yes \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);

	EXPECT_FALSE(fe.Parse("@casesensitive=1 \"ABC\" == \"abc\""));
	EXPECT_EQ(FILTER_ERROR_INVALID_DIRECTIVE, fe.errorCode);
}

TEST_P(FilterExpressionTest, DirectiveWithFileAttributes)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].path = L"abc";
	di.diffFileInfo[0].filename = L"Test.TXT";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].path = L"abc";
	di.diffFileInfo[1].filename = L"test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Default: case-insensitive
	EXPECT_TRUE(fe.Parse("LeftName == \"test.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// @cs: case-sensitive
	EXPECT_TRUE(fe.Parse("@cs LeftName == \"Test.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs LeftName == \"test.txt\""));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs RightName == \"test.txt\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs RightName == \"Test.TXT\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Extension with @cs
	di.diffFileInfo[0].filename = L"file.TXT";
	di.diffFileInfo[1].filename = L"file.txt";

	EXPECT_TRUE(fe.Parse("@cs LeftExtension == \"TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs LeftExtension == \"txt\""));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs RightExtension == \"txt\""));
	EXPECT_TRUE(fe.Evaluate(di));

	// Name contains with @cs
	di.diffFileInfo[0].filename = L"TestFile.TXT";
	di.diffFileInfo[1].filename = L"testfile.txt";

	EXPECT_TRUE(fe.Parse("@cs LeftName contains \"Test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs LeftName contains \"test\""));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs RightName contains \"test\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs RightName contains \"Test\""));
	EXPECT_FALSE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveWithArrays)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Array comparison with @cs
	EXPECT_TRUE(fe.Parse("@cs array(\"abc\", \"def\") == array(\"abc\", \"def\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs array(\"abc\", \"def\") == array(\"ABC\", \"DEF\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs array(\"abc\", \"def\") != array(\"ABC\", \"DEF\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Array with contains and @cs
	EXPECT_TRUE(fe.Parse("@cs array(\"Hello\", \"WORLD\") contains \"Hello\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs array(\"Hello\", \"WORLD\") contains \"hello\""));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array with like and @cs
	EXPECT_TRUE(fe.Parse("@cs array(\"Test.TXT\", \"Test.txt\") like \"*.TXT\""));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs allof(array(\"Test.TXT\", \"Test.txt\") like \"*.TXT\")"));
	EXPECT_FALSE(fe.Evaluate(di));

	// Array with replace and @cs
	EXPECT_TRUE(fe.Parse("@cs replace(array(\"Hello\", \"HELLO\"), \"Hello\", \"Hi\") == array(\"Hi\", \"HELLO\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs replace(array(\"Test\", \"test\"), \"Test\", \"X\") == array(\"X\", \"test\")"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveEmptyExpression)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// Directive only, no expression
	EXPECT_FALSE(fe.Parse("@cs"));
	EXPECT_TRUE(fe.caseSensitive);

	EXPECT_FALSE(fe.Parse("@ci"));
	EXPECT_FALSE(fe.caseSensitive);

	EXPECT_FALSE(fe.Parse("@casesensitive"));
	EXPECT_TRUE(fe.caseSensitive);

	EXPECT_FALSE(fe.Parse("@caseinsensitive"));
	EXPECT_FALSE(fe.caseSensitive);

	// Multiple directives without expression
	EXPECT_FALSE(fe.Parse("@cs"));
	EXPECT_TRUE(fe.caseSensitive);
}

TEST_P(FilterExpressionTest, DirectiveIsWithinAndInRange)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// isWithin with case-sensitive string comparison
	EXPECT_TRUE(fe.Parse("@cs isWithin(\"abc\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs isWithin(\"ABC\", \"AAA\", \"ZZZ\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs isWithin(\"abc\", \"AAA\", \"ZZZ\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Lowercase 'a' > uppercase 'Z' in ASCII

	EXPECT_TRUE(fe.Parse("@cs isWithin(\"ABC\", \"aaa\", \"zzz\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Uppercase 'A' < lowercase 'a'

	// inRange with case-sensitive string comparison
	EXPECT_TRUE(fe.Parse("@cs inRange(\"abc\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs inRange(\"ABC\", \"AAA\", \"ZZZ\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs inRange(\"aaa\", \"aaa\", \"zzz\")"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs inRange(\"zzz\", \"aaa\", \"zzz\")"));
	EXPECT_FALSE(fe.Evaluate(di)); // Exclusive upper bound

	// Array tests with @cs
	EXPECT_TRUE(fe.Parse("@cs isWithin(array(\"abc\", \"ABC\"), \"aaa\", \"zzz\") == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@cs inRange(array(\"abc\", \"ABC\"), \"aaa\", \"zzz\") == array(true, false)"));
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, DirectiveOptimize)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffFileInfo[1].size = 2000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);

	// Default: optimization enabled
	EXPECT_TRUE(fe.Parse("Size > 500"));
	EXPECT_TRUE(fe.optimize);

	// @optimize directive: explicitly enable optimization
	EXPECT_TRUE(fe.Parse("@optimize Size > 500"));
	EXPECT_TRUE(fe.optimize);
	EXPECT_TRUE(fe.Evaluate(di));

	// @opt directive: short form
	EXPECT_TRUE(fe.Parse("@opt Size < 1500"));
	EXPECT_TRUE(fe.optimize);
	EXPECT_TRUE(fe.Evaluate(di));

	// @nooptimize directive: disable optimization
	EXPECT_TRUE(fe.Parse("@nooptimize Size > 500"));
	EXPECT_FALSE(fe.optimize);
	EXPECT_TRUE(fe.Evaluate(di));

	// @noopt directive: short form
	EXPECT_TRUE(fe.Parse("@noopt Size < 1500"));
	EXPECT_FALSE(fe.optimize);
	EXPECT_TRUE(fe.Evaluate(di));

	// Directive only, no expression
	EXPECT_FALSE(fe.Parse("@optimize"));
	EXPECT_TRUE(fe.optimize);

	EXPECT_FALSE(fe.Parse("@nooptimize"));
	EXPECT_FALSE(fe.optimize);

	EXPECT_FALSE(fe.Parse("@opt"));
	EXPECT_TRUE(fe.optimize);

	EXPECT_FALSE(fe.Parse("@noopt"));
	EXPECT_FALSE(fe.optimize);

	// Combined with other directives
	EXPECT_TRUE(fe.Parse("@cs @nooptimize \"ABC\" == \"abc\""));
	EXPECT_TRUE(fe.caseSensitive);
	EXPECT_FALSE(fe.optimize);
	EXPECT_FALSE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@noopt @ci Size > 500"));
	EXPECT_FALSE(fe.caseSensitive);
	EXPECT_FALSE(fe.optimize);
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("@optimize @cs @name=\"Test\" Size > 500"));
	EXPECT_TRUE(fe.caseSensitive);
	EXPECT_TRUE(fe.optimize);
	EXPECT_EQ("Test", fe.name);
	EXPECT_TRUE(fe.Evaluate(di));
}

TEST_P(FilterExpressionTest, StatisticsAndMatchFunctions)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src", L"E:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	CreateSimpleDiffItem(di);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.diritem = false;

	// Create a test provider with varying line data
	// Pane 0: lines contain "1", "2", "3", ..., "10"
	// Pane 1: lines contain "5", "6", "7", ..., "14"
	// Pane 2: lines contain "10", "11", "12", ..., "19"
	struct Provider : public ILineDataProvider
	{
		std::string GetLine(int pane, int lineIndex) const override
		{
			if (pane == 0)
				return std::to_string(lineIndex + 1);
			else if (pane == 1)
				return std::to_string(lineIndex + 5);
			else
				return std::to_string(lineIndex + 10);
		}

		int GetLineCount() const override
		{
			return 10;
		}

		int GetColumnCount(int pane, int lineIndex) const override
		{
			return 3;
		}

		std::string GetColumn(int pane, int lineIndex, int columnIndex) const override
		{
			if (pane == 0)
				return "Col" + std::to_string(columnIndex + 1) + "_" + std::to_string(lineIndex * 10 + columnIndex);
			else if (pane == 1)
				return "Col" + std::to_string(columnIndex + 1) + "_" + std::to_string(lineIndex * 20 + columnIndex);
			else
				return "Col" + std::to_string(columnIndex + 1) + "_" + std::to_string(lineIndex * 30 + columnIndex);
		}

		int GetRealLineNumber(int pane, int lineIndex) const override
		{
			return lineIndex;
		}

		unsigned GetLineFlags(int pane, int lineIndex) const override
		{
			return 0;
		}

		unsigned GetLineEol(int pane, int lineIndex) const override
		{
			return 1;
		}
	} provider;

	auto pFilterSharedContext = std::make_unique<FilterSharedContext>();
	FilterEvalContext ectxt{ &fe, &di, &provider, pFilterSharedContext.get() };

	// maximum function tests - finds maximum value across all lines in the document
	// Provider returns lineIndex+1 for pane 0, so lines are: "1","2","3",...,"10"
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(LeftLineNumber) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(toNumber(LeftLine)) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(MiddleLineNumber) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(RightLineNumber) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// maximum with condition - only consider lines where condition is true
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(LeftLineNumber, LeftLineNumber <= 5) == 5"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(LeftLineNumber, LeftLineNumber > 7) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(toNumber(LeftLine), toNumber(LeftLine) >= 6) == 10"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	// When no lines match condition, returns none
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(LeftLineNumber, LeftLineNumber > 20) == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// minimum function tests - finds minimum value across all lines in the document
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(LeftLineNumber) == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(toNumber(LeftLine)) == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(MiddleLineNumber) == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// minimum with condition
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(LeftLineNumber, LeftLineNumber >= 5) == 5"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(LeftLineNumber, LeftLineNumber < 4) == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(toNumber(LeftLine), toNumber(LeftLine) > 5) == 6"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(LeftLineNumber, LeftLineNumber > 20) == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// average function tests - calculates average across all lines in the document
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(LeftLineNumber) == 5.5"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(toNumber(LeftLine)) == 5.5"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// average with condition - average of lines matching condition
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(LeftLineNumber, LeftLineNumber <= 5) == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(LeftLineNumber, LeftLineNumber > 5) == 8"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(toNumber(LeftLine), toNumber(LeftLine) >= 6) == 8"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("average(LeftLineNumber, LeftLineNumber > 20) == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// matchNumber function tests - returns index of current match within all matches
	// Line 0 contains "1", which matches "1"
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchNumber(LeftLine contains \"1\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	// Line 9 contains "10", which also contains "1", so this is the 2nd match (index 1)
	ectxt.lineIndex = 9;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchNumber(LeftLine contains \"1\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	// Line 4 contains "5"
	ectxt.lineIndex = 4;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchNumber(LeftLine contains \"5\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	// Line 3 doesn't contain "5"
	ectxt.lineIndex = 3;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchNumber(LeftLine contains \"5\") != 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// matchCount function tests - counts total matches across all lines
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchCount(LeftLine contains \"5\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchCount(LeftLine contains \"1\") == 2")); // Matches "1" and "10"
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchCount(LeftLine contains \"99\") == 0"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchCount(toNumber(LeftLine) > 5) == 5"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// MatchDistance function tests - distance to nearest match from current line
	ectxt.lineIndex = 2;  // Line 2 (value "3")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistance(LeftLine contains \"5\") == 2")); // Line 4 has "5"
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 6;  // Line 6 (value "7")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistance(LeftLine contains \"5\") == 2")); // Line 4 has "5", distance is 2
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 4;  // Line 4 (value "5")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistance(LeftLine contains \"5\") == 0")); // Current line matches
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistance(LeftLine contains \"99\") == none")); // No match found
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// MatchDistanceBefore function tests - distance to nearest match before current line
	ectxt.lineIndex = 6;  // Line 6 (value "7")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceBefore(LeftLine contains \"5\") == 2")); // Line 4 has "5"
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 4;  // Line 4 (value "5")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceBefore(LeftLine contains \"5\") == 0")); // Current line matches
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 2;  // Line 2 (value "3")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceBefore(LeftLine contains \"5\") == none")); // No match before
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// MatchDistanceAfter function tests - distance to nearest match after current line
	ectxt.lineIndex = 2;  // Line 2 (value "3")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceAfter(LeftLine contains \"5\") == 2")); // Line 4 has "5"
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 4;  // Line 4 (value "5")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceAfter(LeftLine contains \"5\") == 0")); // Current line matches
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 6;  // Line 6 (value "7")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchDistanceAfter(LeftLine contains \"5\") == none")); // No match after
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// MatchInside function tests - check if current line is between two matches
	ectxt.lineIndex = 3;  // Line 3 (value "4")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchInside(LeftLine contains \"2\", LeftLine contains \"6\") == true")); // Between line 1 ("2") and line 5 ("6")
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 1;  // Line 1 (value "2")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchInside(LeftLine contains \"2\", LeftLine contains \"6\") == true")); // At start boundary
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 5;  // Line 5 (value "6")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchInside(LeftLine contains \"2\", LeftLine contains \"6\") == true")); // At end boundary
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 0;  // Line 0 (value "1")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchInside(LeftLine contains \"2\", LeftLine contains \"6\") == false")); // Before start
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 7;  // Line 7 (value "8")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchInside(LeftLine contains \"2\", LeftLine contains \"6\") == false")); // After end
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// MatchContext function tests - returns lines around a match
	ectxt.lineIndex = 4;  // Line 4 (value "5")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchContext(LeftLine contains \"5\", 1, 1) == true"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchContext(LeftLine contains \"5\", 2, 2) == true"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 0;  // Line 0 (value "1")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchContext(LeftLine contains \"1\", 1, 1) == true"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 9;  // Line 9 (value "10")
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchContext(LeftLine contains \"10\", 1, 1) == true"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	ectxt.lineIndex = 4;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchContext(LeftLine contains \"99\", 1, 1) == false"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Combined tests with statistics functions
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("maximum(LeftLineNumber) > average(LeftLineNumber)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("minimum(LeftLineNumber) < average(LeftLineNumber)"));
	EXPECT_TRUE(fe.Evaluate(ectxt));
}

TEST_P(FilterExpressionTest, BlockFunctions)
{
	PathContext paths(L"C:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src", L"E:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	CreateSimpleDiffItem(di);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;
	fe.diritem = false;

	// Create a test provider with specific line patterns:
	// Lines 0-2: contain "match" (block 1: 3 lines)
	// Lines 3-4: don't match
	// Lines 5-7: contain "match" (block 2: 3 lines)
	// Lines 8-9: don't match
	struct Provider : public ILineDataProvider
	{
		std::string GetLine(int pane, int lineIndex) const override
		{
			if (pane == 0)
			{
				if (lineIndex <= 2 || (lineIndex >= 5 && lineIndex <= 7))
					return "match line " + std::to_string(lineIndex);
				else
					return "other line " + std::to_string(lineIndex);
			}
			else if (pane == 1)
			{
				return "middle " + std::to_string(lineIndex);
			}
			else
			{
				if (lineIndex <= 2 || (lineIndex >= 5 && lineIndex <= 7))
					return "match line " + std::to_string(lineIndex);
				else
					return "other line " + std::to_string(lineIndex);
			}
		}

		int GetLineCount() const override
		{
			return 10;
		}

		int GetColumnCount(int pane, int lineIndex) const override
		{
			return 0;
		}

		std::string GetColumn(int pane, int lineIndex, int columnIndex) const override
		{
			return "";
		}

		int GetRealLineNumber(int pane, int lineIndex) const override
		{
			return lineIndex;
		}

		unsigned GetLineFlags(int pane, int lineIndex) const override
		{
			return 0;
		}

		unsigned GetLineEol(int pane, int lineIndex) const override
		{
			return 1;
		}
	} provider;

	auto pFilterSharedContext = std::make_unique<FilterSharedContext>();
	FilterEvalContext ectxt{ &fe, &di, &provider, pFilterSharedContext.get() };

	// matchBlockCount tests - total number of blocks in the file
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockCount(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 5;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockCount(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Test with no matches
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockCount(LeftLine contains \"nomatch\") == 0"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// matchBlockNumber tests - block index for the current line (1-based)
	// Block 1: lines 0-2
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 2;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Non-matching line should return undefined
	ectxt.lineIndex = 3;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 4;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Block 2: lines 5-7
	ectxt.lineIndex = 5;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 6;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 7;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// matchBlockSize tests - size of the block containing current line
	// Block 1 size: 3 lines (0-2)
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 2;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Non-matching line should return undefined
	ectxt.lineIndex = 3;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Block 2 size: 3 lines (5-7)
	ectxt.lineIndex = 5;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 6;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// matchBlockOffset tests - offset within the block (1-based)
	// Block 1: lines 0-2
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 2;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Non-matching line should return undefined
	ectxt.lineIndex = 3;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Block 2: lines 5-7
	ectxt.lineIndex = 5;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 6;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 7;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 3"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Combined tests - filtering by block properties
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1 and matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1 and matchBlockOffset(LeftLine contains \"match\") == 2"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Filter to show only first line of each block
	ectxt.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 5;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Filter to show only last line of each block
	ectxt.lineIndex = 2;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == matchBlockSize(LeftLine contains \"match\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 1;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == matchBlockSize(LeftLine contains \"match\")"));
	EXPECT_FALSE(fe.Evaluate(ectxt));

	ectxt.lineIndex = 7;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == matchBlockSize(LeftLine contains \"match\")"));
	EXPECT_TRUE(fe.Evaluate(ectxt));

	// Test with all lines matching (entire file is one block)
	struct SingleBlockProvider : public ILineDataProvider
	{
		std::string GetLine(int pane, int lineIndex) const override
		{
			return "match " + std::to_string(lineIndex);
		}

		int GetLineCount() const override
		{
			return 5;
		}

		int GetColumnCount(int pane, int lineIndex) const override
		{
			return 0;
		}

		std::string GetColumn(int pane, int lineIndex, int columnIndex) const override
		{
			return "";
		}

		int GetRealLineNumber(int pane, int lineIndex) const override
		{
			return lineIndex;
		}

		unsigned GetLineFlags(int pane, int lineIndex) const override
		{
			return 0;
		}

		unsigned GetLineEol(int pane, int lineIndex) const override
		{
			return 1;
		}
	} singleBlockProvider;

	FilterEvalContext ectxt2{ &fe, &di, &singleBlockProvider, pFilterSharedContext.get() };

	ectxt2.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt2.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockCount(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt2));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt2.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt2));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt2.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == 5"));
	EXPECT_TRUE(fe.Evaluate(ectxt2));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt2.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 1"));
	EXPECT_TRUE(fe.Evaluate(ectxt2));

	ectxt2.lineIndex = 4;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt2.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == 5"));
	EXPECT_TRUE(fe.Evaluate(ectxt2));

	// Test with no lines matching (no blocks)
	struct NoBlockProvider : public ILineDataProvider
	{
		std::string GetLine(int pane, int lineIndex) const override
		{
			return "other " + std::to_string(lineIndex);
		}

		int GetLineCount() const override
		{
			return 5;
		}

		int GetColumnCount(int pane, int lineIndex) const override
		{
			return 0;
		}

		std::string GetColumn(int pane, int lineIndex, int columnIndex) const override
		{
			return "";
		}

		int GetRealLineNumber(int pane, int lineIndex) const override
		{
			return lineIndex;
		}

		unsigned GetLineFlags(int pane, int lineIndex) const override
		{
			return 0;
		}
		
		unsigned GetLineEol(int pane, int lineIndex) const override
		{
			return 1;
		}
	} noBlockProvider;

	FilterEvalContext ectxt3{ &fe, &di, &noBlockProvider, pFilterSharedContext.get() };

	ectxt3.lineIndex = 0;
	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt3.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockCount(LeftLine contains \"match\") == 0"));
	EXPECT_TRUE(fe.Evaluate(ectxt3));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt3.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockNumber(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt3));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt3.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockSize(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt3));

	pFilterSharedContext = std::make_unique<FilterSharedContext>(); ectxt3.sharedContext = pFilterSharedContext.get();
	EXPECT_TRUE(fe.Parse("matchBlockOffset(LeftLine contains \"match\") == none"));
	EXPECT_TRUE(fe.Evaluate(ectxt3));
}

TEST_P(FilterExpressionTest, StrFindAndRegexFindFunctions)
{
	PathContext paths(L"D:\\dev\\winmerge\\src", L"D:\\dev\\winmerge\\src");
	CDiffContext ctxt(paths, 0);
	DIFFITEM di;
	di.diffFileInfo[0].filename = L"Test.txt";
	di.diffFileInfo[0].size = 1000;
	di.diffFileInfo[1].filename = L"Test.txt";
	di.diffFileInfo[1].size = 1000;
	di.diffcode.setSideFlag(0);
	di.diffcode.setSideFlag(1);

	FilterExpression fe;
	fe.SetDiffContext(&ctxt);
	fe.optimize = GetParam().optimize;

	// strfind function tests - basic search
	EXPECT_TRUE(fe.Parse("strfind(\"Hello World\", \"World\") == 6"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello World\", \"Hello\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello World\", \"NotFound\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// strfind with occurrence number (0-based: 0=first, 1=second, etc.)
	EXPECT_TRUE(fe.Parse("strfind(\"Hello Hello\", \"Hello\", 0) == 0"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello Hello\", \"Hello\", 1) == 6"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello Hello Hello\", \"Hello\", 2) == 12"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello Hello\", \"Hello\", 2) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// strfind case sensitivity (when @ci directive is used)
	fe.caseSensitive = false;
	EXPECT_TRUE(fe.Parse("strfind(\"Hello World\", \"world\") == 6"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(\"Hello World\", \"HELLO\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));
	fe.caseSensitive = true;

	// strfind with arrays
	EXPECT_TRUE(fe.Parse("strfind(array(\"Hello\", \"World\"), \"o\") == array(4, 1)"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("strfind(array(\"Hello\", \"World\"), \"o\", 1) == array(none, none)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexfind function tests - basic regex search
	EXPECT_TRUE(fe.Parse("regexfind(\"Hello123World\", \"[0-9]+\") == 5"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"Hello World\", \"^Hello\") == 0"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"Hello World\", \"[0-9]+\") == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexfind with capture groups
	EXPECT_TRUE(fe.Parse("regexfind(\"Price: $123.45\", \"\\$([0-9]+)\\.([0-9]+)\", 0) == 7"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"Price: $123.45\", \"\\$([0-9]+)\\.([0-9]+)\", 1) == 8"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"Price: $123.45\", \"\\$([0-9]+)\\.([0-9]+)\", 2) == 12"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexfind with occurrence number (0=first match, 1=second match, etc.)
	EXPECT_TRUE(fe.Parse("regexfind(\"abc123def456ghi789\", \"[0-9]+\", 0, 0) == 3"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"abc123def456ghi789\", \"[0-9]+\", 0, 1) == 9"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"abc123def456ghi789\", \"[0-9]+\", 0, 2) == 15"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(\"abc123def456\", \"[0-9]+\", 0, 2) == none"));
	EXPECT_TRUE(fe.Evaluate(di));

	// regexfind case sensitivity
	fe.caseSensitive = false;
	EXPECT_TRUE(fe.Parse("regexfind(\"Hello World\", \"WORLD\") == 6"));
	EXPECT_TRUE(fe.Evaluate(di));
	fe.caseSensitive = true;

	// regexfind with arrays
	EXPECT_TRUE(fe.Parse("regexfind(array(\"abc123\", \"def456\"), \"[0-9]+\") == array(3, 3)"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(array(\"abc123xyz456\", \"def789\"), \"[0-9]+\", 0, 1) == array(9, none)"));
	EXPECT_TRUE(fe.Evaluate(di));

	// Practical use cases
	EXPECT_TRUE(fe.Parse("strfind(LeftName, \".txt\") >= 0"));
	EXPECT_TRUE(fe.Evaluate(di));

	EXPECT_TRUE(fe.Parse("regexfind(LeftName, \"\\.(txt|cpp|h)$\") >= 0"));
	EXPECT_TRUE(fe.Evaluate(di));
}

INSTANTIATE_TEST_SUITE_P(
	OptimizationCases,
	FilterExpressionTest,
	::testing::Values(
		FilterTestParam{ true },
		FilterTestParam{ false }
	)
);

