using MBN_STOCK_WEBVIEW.Playout.Runtime;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class DryRunPlayoutEngineTests
{
    [Fact]
    public async Task DefaultOptions_AreDryRunAndNeverReportARealConnection()
    {
        await using var engine = CreateEngine(PlayoutMode.DryRun);

        var result = await engine.ConnectAsync(CancellationToken.None);

        Assert.Equal(PlayoutMode.DryRun, engine.Status.Mode);
        Assert.Equal(PlayoutConnectionState.DryRunReady, engine.Status.State);
        Assert.True(engine.Status.IsCommandAvailable);
        Assert.False(engine.Status.IsConnected);
        Assert.Equal(
            PlayoutKtapConnectState.NotAttempted,
            engine.Status.LastKtapConnectState);
        Assert.False(engine.Status.KtapConnectAttempted);
        Assert.False(engine.Status.NetworkMonitoringRecordExpected);
        Assert.False(engine.Status.NetworkMonitoringCheckRequired);
        Assert.Null(engine.Status.KtapHelloObserved);
        Assert.Contains("Network Monitoring", engine.Status.Message, StringComparison.Ordinal);
        Assert.True(result.IsSuccess);
        Assert.True(result.IsDryRun);
    }

    [Fact]
    public async Task DisabledMode_RejectsEveryOperationWithoutChangingState()
    {
        await using var engine = CreateEngine(PlayoutMode.Disabled);
        var cue = Cue("disabled-scene");

        var results = new[]
        {
            await engine.ConnectAsync(CancellationToken.None),
            await engine.PrepareAsync(cue, CancellationToken.None),
            await engine.TakeInAsync(CancellationToken.None),
            await engine.NextAsync(cue, CancellationToken.None),
            await engine.UpdateOnAirAsync(cue, CancellationToken.None),
            await engine.TakeOutAsync(
                PlayoutTakeOutScope.All,
                CancellationToken.None),
            await engine.DisconnectAsync(CancellationToken.None)
        };

        Assert.All(results, result => Assert.Equal(PlayoutResultCode.Rejected, result.Code));
        Assert.All(results, result => Assert.False(result.IsDryRun));
        Assert.Equal(PlayoutConnectionState.Disabled, engine.Status.State);
        Assert.False(engine.Status.IsConnected);
        Assert.False(engine.Status.IsCommandAvailable);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);
    }

    [Fact]
    public async Task DryRun_TransitionsPreparedAndOnAirSceneStateInCommandOrder()
    {
        await using var engine = CreateEngine(PlayoutMode.DryRun);
        var statuses = new List<PlayoutStatus>();
        engine.StatusChanged += (_, args) => statuses.Add(args.Current);

        var connect = await engine.ConnectAsync(CancellationToken.None);
        var prepare = await engine.PrepareAsync(
            Cue("first-scene"),
            CancellationToken.None);
        Assert.Equal("first-scene", engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);

        var takeIn = await engine.TakeInAsync(CancellationToken.None);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Equal("first-scene", engine.Status.OnAirSceneName);

        var update = await engine.UpdateOnAirAsync(
            Cue("first-scene"),
            CancellationToken.None);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Equal("first-scene", engine.Status.OnAirSceneName);

        var next = await engine.NextAsync(
            Cue("next-scene"),
            CancellationToken.None);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Equal("next-scene", engine.Status.OnAirSceneName);

        var takeOut = await engine.TakeOutAsync(
            PlayoutTakeOutScope.Layout,
            CancellationToken.None);
        Assert.Null(engine.Status.OnAirSceneName);

        var disconnect = await engine.DisconnectAsync(CancellationToken.None);

        Assert.All(
            new[] { connect, prepare, takeIn, update, next, takeOut, disconnect },
            result => Assert.Equal(PlayoutResultCode.Success, result.Code));
        Assert.All(
            new[] { connect, prepare, takeIn, update, next, takeOut, disconnect },
            result => Assert.True(result.IsDryRun));
        Assert.Equal(7, statuses.Count);
        Assert.True(statuses.Zip(statuses.Skip(1), (left, right) => left.Sequence < right.Sequence).All(x => x));
        Assert.All(statuses, status => Assert.Equal(PlayoutConnectionState.DryRunReady, status.State));
    }

    [Fact]
    public async Task DryRun_TakeInWithoutPreparedSceneIsRejected()
    {
        await using var engine = CreateEngine(PlayoutMode.DryRun);

        var result = await engine.TakeInAsync(CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, result.Code);
        Assert.Null(engine.Status.OnAirSceneName);
    }

    [Fact]
    public async Task DryRun_NextAfterPrepareButBeforeTakeInIsRejected()
    {
        await using var engine = CreateEngine(PlayoutMode.DryRun);
        var idleNext = await engine.NextAsync(
            Cue("next-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, idleNext.Code);
        Assert.Null(engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);

        Assert.True((await engine.PrepareAsync(
            Cue("first-scene"),
            CancellationToken.None)).IsSuccess);

        var next = await engine.NextAsync(
            Cue("next-scene"),
            CancellationToken.None);

        Assert.Equal(PlayoutResultCode.Rejected, next.Code);
        Assert.Equal("first-scene", engine.Status.PreparedSceneName);
        Assert.Null(engine.Status.OnAirSceneName);
    }

    [Fact]
    public async Task CancelledDryRunCommand_DoesNotChangeStatus()
    {
        await using var engine = CreateEngine(PlayoutMode.DryRun);
        var initialStatus = engine.Status;
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        var result = await engine.PrepareAsync(Cue("cancelled-scene"), cancellation.Token);

        Assert.Equal(PlayoutResultCode.Cancelled, result.Code);
        Assert.Same(initialStatus, engine.Status);
        Assert.Null(engine.Status.PreparedSceneName);
    }

    [Fact]
    public async Task DryRun_PublicStatusAndResultDoNotExposeScenePath()
    {
        const string secretPath = "C:\\private-vendor-assets\\do-not-log\\secret.t2s";
        await using var engine = CreateEngine(PlayoutMode.DryRun);
        var cue = new PlayoutCue(secretPath, "C:\\unsafe-scene-name");

        var result = await engine.PrepareAsync(cue, CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.Equal("장면", engine.Status.PreparedSceneName);
        Assert.DoesNotContain(secretPath, result.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(secretPath, engine.Status.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(cue.SceneName, result.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(cue.SceneName, engine.Status.Message, StringComparison.OrdinalIgnoreCase);
    }

    private static PlayoutCue Cue(string sceneName) => new(
        $"C:\\test-only\\{sceneName}.t2s",
        sceneName,
        [new PlayoutField("headline", "safe value", true)]);

    private static TornadoPlayoutEngine CreateEngine(PlayoutMode mode)
    {
        var options = ValidatedPlayoutOptions.Create(new PlayoutOptions { Mode = mode });
        return new TornadoPlayoutEngine(
            options,
            new FakeK3dSessionFactory(),
            new FakeRegistrationProbe(),
            new FakeTornadoProcessProbe(new TornadoProcessSnapshot(0, 0, 0)),
            null,
            new FakeLiveAuthorization(false),
            TimeProvider.System,
            startMonitor: false);
    }
}
