using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData;
using LegacyExpressions = GitHub.DistributedTask.Expressions2;
using Moq;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Xunit;

namespace GitHub.Runner.Common.Tests.Worker
{
    public sealed class PipelineTemplateEvaluatorWrapperL0
    {
        private CancellationTokenSource _ecTokenSource;
        private CancellationTokenSource _rootTokenSource;
        private Mock<IExecutionContext> _ec;
        private TestHostContext _hc;

        // -------------------------------------------------------------------
        // EvaluateAndCompare core behavior
        // -------------------------------------------------------------------

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_DoesNotRecordMismatch_WhenResultsMatch()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                var token = new StringToken(null, null, null, "test-value");
                var contextData = new DictionaryContextData();
                var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateStepDisplayName(token, contextData, expressionFunctions);

                Assert.Equal("test-value", result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_SkipsMismatchRecording_WhenCancellationOccursDuringEvaluation()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Call EvaluateAndCompare directly: the new evaluator cancels the root token
                // and returns a different value, forcing hasMismatch = true.
                // Because cancellation flipped during the evaluation window, the
                // mismatch should be skipped.
                var result = wrapper.EvaluateAndCompare<string, string>(
                    "TestCancellationSkip",
                    () => "legacy-value",
                    () =>
                    {
                        _rootTokenSource.Cancel();
                        return "different-value";
                    },
                    (legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));

                Assert.Equal("legacy-value", result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_SkipsMismatchRecording_WhenRootCancellationOccursBetweenEvaluators()
        {
            // Simulates job-level cancellation firing between legacy and new evaluator runs.
            // Root is mocked with a separate CancellationTokenSource to exercise the
            // _context.Root?.CancellationToken path (the job-level token).
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Legacy evaluator cancels the root token (simulating job cancel) and returns a value.
                // The new evaluator returns a different value. The mismatch should be skipped.
                var result = wrapper.EvaluateAndCompare<string, string>(
                    "TestRootCancellationSkip",
                    () =>
                    {
                        var legacyValue = "legacy-value";
                        _rootTokenSource.Cancel();
                        return legacyValue;
                    },
                    () => "different-value",
                    (legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));

                Assert.Equal("legacy-value", result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_RecordsMismatch_WhenResultsDifferWithoutCancellation()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Different results without cancellation — mismatch SHOULD be recorded.
                var result = wrapper.EvaluateAndCompare<string, string>(
                    "TestMismatchRecorded",
                    () => "legacy-value",
                    () => "different-value",
                    (legacy, @new) => string.Equals(legacy, @new, StringComparison.Ordinal));

                Assert.Equal("legacy-value", result);
                Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        // -------------------------------------------------------------------
        // Smoke tests — both parsers agree, no mismatch recorded
        // -------------------------------------------------------------------

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateStepContinueOnError_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new BooleanToken(null, null, null, true);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateStepContinueOnError(token, contextData, functions);

                Assert.True(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateStepEnvironment_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "FOO"), new StringToken(null, null, null, "bar"));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateStepEnvironment(token, contextData, functions, StringComparer.OrdinalIgnoreCase);

                Assert.NotNull(result);
                Assert.Equal("bar", result["FOO"]);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateStepIf_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new BasicExpressionToken(null, null, null, "true");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();
                var expressionState = new List<KeyValuePair<string, object>>();

                var result = wrapper.EvaluateStepIf(token, contextData, functions, expressionState);

                Assert.True(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateStepInputs_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "input1"), new StringToken(null, null, null, "val1"));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateStepInputs(token, contextData, functions);

                Assert.NotNull(result);
                Assert.Equal("val1", result["input1"]);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateStepTimeout_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new NumberToken(null, null, null, 10);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateStepTimeout(token, contextData, functions);

                Assert.Equal(10, result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_EmptyImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new StringToken(null, null, null, "");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_DockerPrefixOnly_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new StringToken(null, null, null, "docker://");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_DockerPrefixOnlyMapping_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_EmptyImageMapping_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_ValidImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new StringToken(null, null, null, "ubuntu:latest");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.NotNull(result);
                Assert.Equal("ubuntu:latest", result.Image);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobContainer_DockerPrefixWithImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new StringToken(null, null, null, "docker://ubuntu:latest");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobContainer(token, contextData, functions);

                Assert.NotNull(result);
                Assert.Equal("ubuntu:latest", result.Image);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobOutput_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "out1"), new StringToken(null, null, null, "val1"));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobOutput(token, contextData, functions);

                Assert.NotNull(result);
                Assert.Equal("val1", result["out1"]);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateEnvironmentUrl_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new StringToken(null, null, null, "https://example.com");
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateEnvironmentUrl(token, contextData, functions);

                Assert.NotNull(result);
                var stringResult = result as StringToken;
                Assert.NotNull(stringResult);
                Assert.Equal("https://example.com", stringResult.Value);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobDefaultsRun_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var token = new MappingToken(null, null, null);
                token.Add(new StringToken(null, null, null, "shell"), new StringToken(null, null, null, "bash"));
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobDefaultsRun(token, contextData, functions);

                Assert.NotNull(result);
                Assert.Equal("bash", result["shell"]);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_Null_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(null, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_EmptyImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                // Build a services mapping token with one service whose image is empty string
                // Similar to: services: { db: { image: '' } }
                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, ""));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);

                // Should get a list with one entry where the container is null (empty image = no container)
                Assert.NotNull(result);
                Assert.Single(result);
                Assert.Equal("db", result[0].Key);
                Assert.Null(result[0].Value);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_DockerPrefixOnlyImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "docker://"));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);

                Assert.NotNull(result);
                Assert.Single(result);
                Assert.Equal("db", result[0].Key);
                Assert.Null(result[0].Value);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_ExpressionEvalsToEmpty_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                // Simulates: services: { db: { image: ${{ condition && 'img' || '' }} } }
                // where the expression evaluates to '' at runtime
                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new BasicExpressionToken(null, null, null, "''"));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);

                Assert.NotNull(result);
                Assert.Single(result);
                Assert.Equal("db", result[0].Key);
                Assert.Null(result[0].Value);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_ValidImage_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);

                Assert.NotNull(result);
                Assert.Single(result);
                Assert.Equal("db", result[0].Key);
                Assert.NotNull(result[0].Value);
                Assert.Equal("postgres:latest", result[0].Value.Image);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_EntrypointAndCommand_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
                serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
                serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: true);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions);

                Assert.NotNull(result);
                Assert.Single(result);
                Assert.Equal("db", result[0].Key);
                Assert.NotNull(result[0].Value);
                Assert.Equal("postgres:latest", result[0].Value.Image);
                Assert.Equal("/bin/bash", result[0].Value.Entrypoint);
                Assert.Equal("-lc echo hi", result[0].Value.Command);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobServiceContainers_EntrypointAndCommand_FlagOff_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var servicesMapping = new MappingToken(null, null, null);
                var serviceMapping = new MappingToken(null, null, null);
                serviceMapping.Add(new StringToken(null, null, null, "image"), new StringToken(null, null, null, "postgres:latest"));
                serviceMapping.Add(new StringToken(null, null, null, "entrypoint"), new StringToken(null, null, null, "/bin/bash"));
                serviceMapping.Add(new StringToken(null, null, null, "command"), new StringToken(null, null, null, "-lc echo hi"));
                servicesMapping.Add(new StringToken(null, null, null, "db"), serviceMapping);

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                Assert.Throws<GitHub.DistributedTask.ObjectTemplating.TemplateValidationException>(() =>
                    wrapper.EvaluateJobServiceContainers(servicesMapping, contextData, functions));
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateJobSnapshotRequest_Null_BothParsersAgree()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);
                var contextData = new DictionaryContextData();
                var functions = new List<LegacyExpressions.IFunctionInfo>();

                var result = wrapper.EvaluateJobSnapshotRequest(null, contextData, functions);

                Assert.Null(result);
                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        // -------------------------------------------------------------------
        // JSON parse error equivalence via EvaluateAndCompare
        // -------------------------------------------------------------------

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_JsonReaderExceptions_TreatedAsEquivalent()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Both throw JsonReaderException with different messages — should be treated as equivalent
                var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken from JsonReader. Path '', line 0, position 0.");
                var newEx = new Newtonsoft.Json.JsonReaderException("Error parsing fromJson", new Newtonsoft.Json.JsonReaderException("Unexpected end"));

                Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
                    wrapper.EvaluateAndCompare<string, string>(
                        "TestJsonEquivalence",
                        () => throw legacyEx,
                        () => throw newEx,
                        (a, b) => string.Equals(a, b, StringComparison.Ordinal)));

                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_MixedJsonExceptionTypes_TreatedAsEquivalent()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Legacy throws Newtonsoft JsonReaderException, new throws System.Text.Json.JsonException
                var legacyEx = new Newtonsoft.Json.JsonReaderException("Error reading JToken");
                var newEx = new System.Text.Json.JsonException("Error parsing fromJson");

                Assert.Throws<Newtonsoft.Json.JsonReaderException>(() =>
                    wrapper.EvaluateAndCompare<string, string>(
                        "TestMixedJsonTypes",
                        () => throw legacyEx,
                        () => throw newEx,
                        (a, b) => string.Equals(a, b, StringComparison.Ordinal)));

                Assert.False(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        [Fact]
        [Trait("Level", "L0")]
        [Trait("Category", "Worker")]
        public void EvaluateAndCompare_NonJsonExceptions_RecordsMismatch()
        {
            try
            {
                Setup();
                _ec.Object.Global.Variables.Set(Constants.Runner.Features.CompareWorkflowParser, "true");

                var wrapper = new PipelineTemplateEvaluatorWrapper(_hc, _ec.Object, allowServiceContainerCommand: false);

                // Both throw non-JSON exceptions with different messages — should record mismatch
                var legacyEx = new InvalidOperationException("some error");
                var newEx = new InvalidOperationException("different error");

                Assert.Throws<InvalidOperationException>(() =>
                    wrapper.EvaluateAndCompare<string, string>(
                        "TestNonJsonMismatch",
                        () => throw legacyEx,
                        () => throw newEx,
                        (a, b) => string.Equals(a, b, StringComparison.Ordinal)));

                Assert.True(_ec.Object.Global.HasTemplateEvaluatorMismatch);
            }
            finally
            {
                Teardown();
            }
        }

        // -------------------------------------------------------------------
        // Helpers
        // -------------------------------------------------------------------

        private void Setup([CallerMemberName] string name = "")
        {
            _ecTokenSource?.Dispose();
            _ecTokenSource = new CancellationTokenSource();
            _rootTokenSource?.Dispose();
            _rootTokenSource = new CancellationTokenSource();

            _hc = new TestHostContext(this, name);

            var expressionValues = new LegacyContextData.DictionaryContextData();
            var expressionFunctions = new List<LegacyExpressions.IFunctionInfo>();

            _ec = new Mock<IExecutionContext>();
            _ec.Setup(x => x.Global)
                .Returns(new GlobalContext
                {
                    FileTable = new List<String>(),
                    Variables = new Variables(_hc, new Dictionary<string, VariableValue>()),
                    WriteDebug = true,
                });
            _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
            var rootEc = new Mock<IExecutionContext>();
            rootEc.Setup(x => x.CancellationToken).Returns(_rootTokenSource.Token);
            _ec.Setup(x => x.Root).Returns(rootEc.Object);
            _ec.Setup(x => x.ExpressionValues).Returns(expressionValues);
            _ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions);
            _ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
            _ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<ExecutionContextLogOptions>())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); });
        }

        private void Teardown()
        {
            _ecTokenSource?.Dispose();
            _rootTokenSource?.Dispose();
            _hc?.Dispose();
        }
    }
}
