using System.Collections;
using System.Text.Json;
using System.Text.Json.Serialization;
using MBN_STOCK_WEBVIEW.Core.Playout;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.PlayoutSmoke;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class IsolatedTestCommandTests : IDisposable
{
    private readonly TemporarySceneDirectory _scenes =
        TemporarySceneDirectory.Create("5001.t2s", "5006.t2s");

    [Fact]
    public void Preflight_ValidSequenceBuildsExactBasenameCuesAndDisablesReconnect()
    {
        using var environment = ClearedPlayoutEnvironment();
        using var config = CreateConfig(ValidOptions());

        var plan = IsolatedTestCommandPreflight.Create(Invocation(
            SmokeCommandKind.TestSequence,
            config.Path));

        Assert.Equal("5001.t2s", plan.PrepareCue!.SceneFile);
        Assert.Equal("5001", plan.PrepareCue.SceneName);
        Assert.Equal("5006.t2s", plan.NextCue!.SceneFile);
        Assert.Equal("5006", plan.NextCue.SceneName);
        Assert.Equal(TimeSpan.FromSeconds(1), plan.ObservationDuration);
        Assert.False(plan.Options.ReconnectEnabled);
        Assert.Equal(0, plan.Options.MaximumReconnectAttempts);
    }

    [Fact]
    public void Preflight_TestConnectReusesTestConfigurationGatesWithoutScenes()
    {
        using var environment = ClearedPlayoutEnvironment();
        using var config = CreateConfig(ValidOptions());

        var plan = IsolatedTestCommandPreflight.Create(new SmokeCommandInvocation(
            SmokeCommandKind.TestConnect,
            config.Path));

        Assert.Null(plan.PrepareCue);
        Assert.Null(plan.NextCue);
        Assert.Equal(TimeSpan.Zero, plan.ObservationDuration);
        Assert.Equal(PlayoutMode.Test, plan.Options.Mode);
    }

    [Fact]
    public void Preflight_RejectsAnyPlayoutEnvironmentOverrideBeforeReadingConfiguration()
    {
        using var environment = ClearedPlayoutEnvironment()
            .Set("MBN_STOCK_PLAYOUT_FUTURE_OVERRIDE", "sensitive-value");

        var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
            IsolatedTestCommandPreflight.Create(Invocation(
                SmokeCommandKind.TestSequence,
                "C:\\sensitive\\missing.local.json")));

        Assert.Equal("environment-override-present", exception.ErrorCode);
        Assert.DoesNotContain("sensitive", exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Preflight_RejectsRelativeOrMissingConfigWithoutReflectingPath()
    {
        using var environment = ClearedPlayoutEnvironment();
        const string unsafePath = ".\\private\\playout.local.json";

        var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
            IsolatedTestCommandPreflight.Create(Invocation(
                SmokeCommandKind.TestSequence,
                unsafePath)));

        Assert.Equal("config-file-rejected", exception.ErrorCode);
        Assert.DoesNotContain(unsafePath, exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Theory]
    [InlineData(PlayoutMode.DryRun, false)]
    [InlineData(PlayoutMode.Live, false)]
    [InlineData(PlayoutMode.Test, true)]
    public void Preflight_RejectsAnythingThatCouldEnableLiveOutput(
        PlayoutMode mode,
        bool trustedLiveOutputEnabled)
    {
        using var environment = ClearedPlayoutEnvironment();
        var options = ValidOptions();
        options.Mode = mode;
        options.TrustedLiveOutputEnabled = trustedLiveOutputEnabled;
        using var config = CreateConfig(options);

        var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
            IsolatedTestCommandPreflight.Create(Invocation(
                SmokeCommandKind.TestSequence,
                config.Path)));

        Assert.Equal("test-mode-required", exception.ErrorCode);
    }

    [Fact]
    public void Preflight_ReusesAllowlistAndExistingFileValidationBeforeEngineCreation()
    {
        using var environment = ClearedPlayoutEnvironment();
        var options = ValidOptions();
        options.TestSceneAllowlist = ["5001"];
        using var config = CreateConfig(options);

        var exception = Assert.Throws<IsolatedTestPreflightException>(() =>
            IsolatedTestCommandPreflight.Create(Invocation(
                SmokeCommandKind.TestSequence,
                config.Path)));

        Assert.Equal("safety-gate-rejected", exception.ErrorCode);
        Assert.DoesNotContain("5006", exception.Message, StringComparison.Ordinal);
        Assert.DoesNotContain(_scenes.Path, exception.Message, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Executor_SuccessRunsExactSequenceWithObservationAndAllScopeTakeOut()
    {
        var engine = new RecordingPlayoutEngine();
        var delays = new List<TimeSpan>();

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (delay, _) =>
            {
                delays.Add(delay);
                return Task.CompletedTask;
            });

        Assert.Equal(
        [
            "Connect",
            "Prepare",
            "TakeIn",
            "Next",
            "TakeOut:All",
            "Disconnect",
            "Dispose"
        ], engine.Calls);
        Assert.Equal(
            [
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(1)
            ],
            delays);
        Assert.True(report.Completed);
        Assert.True(report.ComActivationAttempted);
        Assert.Equal("accepted-unconfirmed", report.LastKtapConnectState);
        Assert.True(report.KtapConnectAttempted);
        Assert.True(report.KtapConnectAccepted);
        Assert.True(report.NetworkMonitoringRecordExpected);
        Assert.True(report.NetworkMonitoringCheckRequired);
        Assert.Null(report.NetworkMonitoringVerified);
        Assert.False(report.OutcomeUnknown);
        Assert.Equal(6, report.Steps.Count);
        Assert.True(report.Cleanup.AdapterDisposed);
    }

    [Fact]
    public async Task Executor_TestConnectNeverIssuesSceneOrOutputMutation()
    {
        var engine = new RecordingPlayoutEngine();

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestConnect),
            _ => engine,
            (_, _) => throw new InvalidOperationException("No observation is allowed."));

        Assert.Equal(["Connect", "Disconnect", "Dispose"], engine.Calls);
        Assert.True(report.Completed);
        Assert.True(report.ComActivationAttempted);
        Assert.Equal("accepted-unconfirmed", report.LastKtapConnectState);
        Assert.True(report.NetworkMonitoringRecordExpected);
        Assert.True(report.NetworkMonitoringCheckRequired);
    }

    [Fact]
    public async Task Executor_ConnectUnavailableDoesNotGuessWhetherComWasAttempted()
    {
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.Connect, PlayoutResultCode.Unavailable);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestConnect),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Null(report.ComActivationAttempted);
        Assert.Equal("not-attempted", report.LastKtapConnectState);
        Assert.False(report.NetworkMonitoringRecordExpected);
        Assert.False(report.NetworkMonitoringCheckRequired);
        Assert.Equal(["Connect", "Disconnect", "Dispose"], engine.Calls);
    }

    [Fact]
    public async Task Executor_UnknownConnectOutcomeReportsUnknownActivationAndSkipsDisconnect()
    {
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.Connect, PlayoutResultCode.OutcomeUnknown);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestConnect),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Null(report.ComActivationAttempted);
        Assert.Equal("attempted", report.LastKtapConnectState);
        Assert.True(report.KtapConnectAttempted);
        Assert.Null(report.KtapConnectAccepted);
        Assert.Null(report.NetworkMonitoringRecordExpected);
        Assert.True(report.NetworkMonitoringCheckRequired);
        Assert.True(report.OutcomeUnknown);
        Assert.Equal(["Connect", "Quarantine", "Dispose"], engine.Calls);
        Assert.True(report.Cleanup.QuarantineCompleted);
    }

    [Fact]
    public async Task Executor_EngineCreationFailureReportsNoComActivationAttempt()
    {
        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestConnect),
            _ => throw new InvalidOperationException("sensitive factory failure"),
            (_, _) => Task.CompletedTask);

        Assert.False(report.ConnectRequestIssued);
        Assert.False(report.ComActivationAttempted);
        Assert.False(report.Completed);
        Assert.False(report.OutcomeUnknown);
    }

    [Fact]
    public async Task Executor_IntermediateFailureStopsPlaybackAndPerformsDisconnectCleanup()
    {
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.Prepare, PlayoutResultCode.Failed);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Equal(["Connect", "Prepare", "Disconnect", "Dispose"], engine.Calls);
        Assert.False(report.Completed);
        Assert.Equal("Prepare", report.StoppedAfter);
        Assert.Contains(report.Steps, step =>
            step.Phase == "cleanup" && step.Operation == "Disconnect");
    }

    [Fact]
    public async Task Executor_OutcomeUnknownNeverAttemptsAutomaticDisconnectOrLaterCommand()
    {
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.TakeIn, PlayoutResultCode.OutcomeUnknown);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Equal(["Connect", "Prepare", "TakeIn", "Quarantine", "Dispose"], engine.Calls);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.OutputMayBeActive);
        Assert.False(report.Cleanup.DisconnectAttempted);
        Assert.True(report.Cleanup.AdapterDisposed);
    }

    [Fact]
    public async Task Executor_QuarantineFailureSkipsDisposeToAvoidImplicitDisconnect()
    {
        var engine = new RecordingPlayoutEngine
        {
            ThrowOnQuarantine = true
        }.Return(PlayoutOperation.TakeIn, PlayoutResultCode.OutcomeUnknown);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Equal(["Connect", "Prepare", "TakeIn", "Quarantine"], engine.Calls);
        Assert.True(report.Cleanup.QuarantineAttempted);
        Assert.False(report.Cleanup.QuarantineCompleted);
        Assert.False(report.Cleanup.AdapterDisposed);
        Assert.False(report.Cleanup.DisconnectAttempted);
    }

    [Fact]
    public async Task Executor_CancelledObservationStopsBeforeNextWithoutMarkingComOutcomeUnknown()
    {
        using var cancellation = new CancellationTokenSource();
        var engine = new RecordingPlayoutEngine();

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, token) =>
            {
                cancellation.Cancel();
                return Task.FromCanceled(token);
            },
            cancellation.Token);

        Assert.Equal(
            ["Connect", "Prepare", "TakeIn", "TakeOut:All", "Disconnect", "Dispose"],
            engine.Calls);
        Assert.Equal("Observation", report.StoppedAfter);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.OutputMayBeActive);
        Assert.False(report.Completed);
        Assert.Contains(report.Steps, step =>
            step.Phase == "cleanup" && step.Operation == "TakeOut");
    }

    [Fact]
    public async Task Executor_UnknownTakeOutCleanupSkipsAutomaticDisconnect()
    {
        using var cancellation = new CancellationTokenSource();
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.TakeOut, PlayoutResultCode.OutcomeUnknown);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, token) =>
            {
                cancellation.Cancel();
                return Task.FromCanceled(token);
            },
            cancellation.Token);

        Assert.Equal(
            ["Connect", "Prepare", "TakeIn", "TakeOut:All", "Quarantine", "Dispose"],
            engine.Calls);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.OutputMayBeActive);
        Assert.False(report.Cleanup.DisconnectAttempted);
    }

    [Theory]
    [InlineData(PlayoutResultCode.Failed)]
    [InlineData(PlayoutResultCode.Unavailable)]
    [InlineData(PlayoutResultCode.Cancelled)]
    [InlineData(PlayoutResultCode.Rejected)]
    public async Task Executor_NonSuccessfulCleanupTakeOutReportsPossibleActiveOutput(
        PlayoutResultCode takeOutCode)
    {
        using var cancellation = new CancellationTokenSource();
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.TakeOut, takeOutCode);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, token) =>
            {
                cancellation.Cancel();
                return Task.FromCanceled(token);
            },
            cancellation.Token);

        Assert.Equal(
            ["Connect", "Prepare", "TakeIn", "TakeOut:All", "Quarantine", "Dispose"],
            engine.Calls);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.OutputMayBeActive);
        Assert.False(report.Cleanup.DisconnectAttempted);
    }

    [Theory]
    [InlineData(PlayoutResultCode.Failed)]
    [InlineData(PlayoutResultCode.Unavailable)]
    [InlineData(PlayoutResultCode.Cancelled)]
    [InlineData(PlayoutResultCode.Rejected)]
    public async Task Executor_NonSuccessfulSequenceTakeOutReportsPossibleActiveOutput(
        PlayoutResultCode takeOutCode)
    {
        var engine = new RecordingPlayoutEngine()
            .Return(PlayoutOperation.TakeOut, takeOutCode);

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.CompletedTask);

        Assert.Equal(
            ["Connect", "Prepare", "TakeIn", "Next", "TakeOut:All", "Quarantine", "Dispose"],
            engine.Calls);
        Assert.True(report.OutcomeUnknown);
        Assert.True(report.OutputMayBeActive);
        Assert.False(report.Cleanup.DisconnectAttempted);
    }

    [Fact]
    public async Task Executor_ObservationFailureStillPerformsTakeOutCleanup()
    {
        var engine = new RecordingPlayoutEngine();

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.FromException(new InvalidOperationException("fake observation failure")));

        Assert.Equal(
            ["Connect", "Prepare", "TakeIn", "TakeOut:All", "Disconnect", "Dispose"],
            engine.Calls);
        Assert.Equal("Observation", report.StoppedAfter);
        Assert.False(report.OutcomeUnknown);
        Assert.False(report.OutputMayBeActive);
        Assert.True(report.Cleanup.DisconnectAttempted);
    }

    [Fact]
    public async Task Executor_ReportDoesNotContainEngineMessagesPathsOrSceneCodes()
    {
        var engine = new RecordingPlayoutEngine("C:\\private\\5001.t2s HRESULT 0x80004005");

        var report = await IsolatedTestCommandExecutor.ExecuteAsync(
            ExecutionPlan(SmokeCommandKind.TestSequence),
            _ => engine,
            (_, _) => Task.CompletedTask);
        var json = JsonSerializer.Serialize(report);

        Assert.DoesNotContain("private", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("5001", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("HRESULT", json, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("0x80004005", json, StringComparison.OrdinalIgnoreCase);
    }

    public void Dispose() => _scenes.Dispose();

    private IsolatedTestCommandPlan ExecutionPlan(SmokeCommandKind kind) => new(
        kind,
        ValidOptions(),
        kind == SmokeCommandKind.TestSequence ? new PlayoutCue("5001.t2s", "5001") : null,
        kind == SmokeCommandKind.TestSequence ? new PlayoutCue("5006.t2s", "5006") : null,
        kind == SmokeCommandKind.TestSequence ? TimeSpan.FromSeconds(1) : TimeSpan.Zero);

    private SmokeCommandInvocation Invocation(SmokeCommandKind kind, string configPath) => new(
        kind,
        configPath,
        "5001",
        "5006",
        1000);

    private PlayoutOptions ValidOptions() => new()
    {
        Mode = PlayoutMode.Test,
        Host = "127.0.0.1",
        Port = 30001,
        TcpMode = 1,
        ClientPort = 0,
        OutputChannel = 9,
        LayoutIndex = 10,
        SceneDirectory = _scenes.Path,
        TestProcessWindowTitlePattern = "^Tornado2 TEST$",
        TestSceneAllowlist = ["5001", "5006"],
        TrustedLiveOutputEnabled = false,
        ReconnectEnabled = true,
        MaximumReconnectAttempts = 3
    };

    private static TemporaryJsonFile CreateConfig(PlayoutOptions options)
    {
        var serializerOptions = new JsonSerializerOptions();
        serializerOptions.Converters.Add(new JsonStringEnumConverter());
        return TemporaryJsonFile.Create(JsonSerializer.Serialize(options, serializerOptions));
    }

    private static EnvironmentScope ClearedPlayoutEnvironment()
    {
        var result = new EnvironmentScope();
        foreach (DictionaryEntry entry in Environment.GetEnvironmentVariables())
        {
            if (entry.Key is string name &&
                name.StartsWith(
                    IsolatedTestCommandPreflight.EnvironmentVariablePrefix,
                    StringComparison.OrdinalIgnoreCase))
            {
                result.Set(name, null);
            }
        }

        return result;
    }

    private sealed class RecordingPlayoutEngine : IPlayoutEngine
    {
        private readonly Dictionary<PlayoutOperation, PlayoutResultCode> _codes = [];
        private readonly string _message;

        public RecordingPlayoutEngine(string message = "safe")
        {
            _message = message;
            Status = new PlayoutStatus(
                PlayoutMode.Test,
                PlayoutConnectionState.Disconnected,
                false,
                true,
                false,
                false,
                null,
                null,
                "safe",
                DateTimeOffset.UnixEpoch,
                1);
        }

        public List<string> Calls { get; } = [];

        public bool ThrowOnQuarantine { get; init; }

        public PlayoutStatus Status { get; private set; }

        public event EventHandler<PlayoutStatusChangedEventArgs>? StatusChanged
        {
            add { }
            remove { }
        }

        public RecordingPlayoutEngine Return(
            PlayoutOperation operation,
            PlayoutResultCode code)
        {
            _codes[operation] = code;
            return this;
        }

        public Task<PlayoutResult> ConnectAsync(CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.Connect, "Connect");

        public Task<PlayoutResult> DisconnectAsync(CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.Disconnect, "Disconnect");

        public Task<PlayoutResult> PrepareAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.Prepare, "Prepare");

        public Task<PlayoutResult> TakeInAsync(CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.TakeIn, "TakeIn");

        public Task<PlayoutResult> NextAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.Next, "Next");

        public Task<PlayoutResult> UpdateOnAirAsync(
            PlayoutCue cue,
            CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.UpdateOnAir, "UpdateOnAir");

        public Task<PlayoutResult> TakeOutAsync(
            PlayoutTakeOutScope scope,
            CancellationToken cancellationToken = default) =>
            Result(PlayoutOperation.TakeOut, $"TakeOut:{scope}");

        public ValueTask DisposeAsync()
        {
            Calls.Add("Dispose");
            return ValueTask.CompletedTask;
        }

        public ValueTask QuarantineAsync()
        {
            Calls.Add("Quarantine");
            if (ThrowOnQuarantine)
            {
                throw new InvalidOperationException("fake quarantine failure");
            }

            return ValueTask.CompletedTask;
        }

        private Task<PlayoutResult> Result(PlayoutOperation operation, string call)
        {
            Calls.Add(call);
            var code = _codes.GetValueOrDefault(operation, PlayoutResultCode.Success);
            if (operation == PlayoutOperation.Connect)
            {
                Status = Status with
                {
                    LastKtapConnectState = code switch
                    {
                        PlayoutResultCode.Success => PlayoutKtapConnectState.AcceptedUnconfirmed,
                        PlayoutResultCode.Failed => PlayoutKtapConnectState.Failed,
                        PlayoutResultCode.OutcomeUnknown or PlayoutResultCode.TimedOut =>
                            PlayoutKtapConnectState.Attempted,
                        _ => PlayoutKtapConnectState.NotAttempted
                    }
                };
            }

            return Task.FromResult(new PlayoutResult(
                operation,
                code,
                false,
                _message,
                DateTimeOffset.UnixEpoch));
        }
    }
}
