using System.Security;
using System.Text;
using MBN_STOCK_WEBVIEW.Playout.Configuration;
using MBN_STOCK_WEBVIEW.Playout.Diagnostics;
using MBN_STOCK_WEBVIEW.Playout.Safety;

namespace MBN_STOCK_WEBVIEW.Playout.Tests;

public sealed class DevelopmentLiveLaunchBootstrapTests
{
    private const string NativeSha256 =
        "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    private const string InteropSha256 =
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";

    [Theory]
    [InlineData("--development-live", "ignored", "--development-live")]
    [InlineData("unexpected", "--development-live", "unexpected")]
    [InlineData(" ", "--development-live", " ")]
    public void ResolveLaunchArguments_PreservesNonEmptyActivationArguments(
        string activationArguments,
        string processArgument,
        string expected)
    {
        Assert.Equal(
            expected,
            DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
                activationArguments,
                ["app.exe", processArgument]));
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    public void ResolveLaunchArguments_UsesOneProcessArgumentWhenActivationIsEmpty(
        string? activationArguments)
    {
        Assert.Equal(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
                activationArguments,
                ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument]));
    }

    [Fact]
    public void ResolveLaunchArguments_DistinguishesNormalLaunchFromAdditionalArguments()
    {
        Assert.Null(DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments("", ["app.exe"]));
        var additionalArguments = DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
            "",
            ["app.exe", DevelopmentLiveLaunchBootstrap.ExactLaunchArgument, "extra"]);

        Assert.NotNull(additionalArguments);
        Assert.NotEqual(string.Empty, additionalArguments);
        Assert.NotEqual(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            additionalArguments);
    }

    [Fact]
    public void DefaultPath_IsASeparateLocalApplicationDataFile()
    {
        var expected = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "MBN_STOCK_WEBVIEW",
            "Config",
            "playout.development-live.local.json");

        Assert.Equal(expected, DevelopmentLiveLaunchBootstrap.DefaultPath);
        Assert.NotEqual(PlayoutOptionsLoader.DefaultPath,
            DevelopmentLiveLaunchBootstrap.DefaultPath);
    }

    [Theory]
    [InlineData("--development-live ")]
    [InlineData(" --development-live")]
    [InlineData("--DEVELOPMENT-LIVE")]
    [InlineData("--development-live --another-option")]
    public void NonExactArgument_IsRejectedWithoutReadingOrMutating(string? arguments)
    {
        var platform = FakePlatform.Valid();

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            arguments,
            isDebugBuild: true,
            "ignored.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-arguments-invalid", result.Code);
        Assert.False(result.IsLaunchAllowed);
        Assert.True(result.IsRequested);
        Assert.Equal(0, platform.ReadCount);
        Assert.Empty(platform.Environment);
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public void GateALaunch_BypassesOrdinaryAuthorizationWithoutEnvironmentMutation(
        bool isDebugBuild)
    {
        var platform = FakePlatform.Valid();
        platform.Environment[
            PlayoutLaunchAuthorization.GateACapabilityEnvironmentVariable] =
            new string('A', 64);
        var originalEnvironment = platform.Environment.ToArray();

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            null,
            isDebugBuild,
            "must-not-be-read.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.GateABypassed, result.Status);
        Assert.Equal("development-live-gate-a-bypassed", result.Code);
        Assert.True(result.IsLaunchAllowed);
        Assert.False(result.IsRequested);
        Assert.Equal(0, platform.ReadCount);
        Assert.Equal(originalEnvironment, platform.Environment);
    }

    [Theory]
    [InlineData(null, true)]
    [InlineData("", true)]
    [InlineData("--development-live", true)]
    [InlineData(null, false)]
    [InlineData("", false)]
    [InlineData("--development-live", false)]
    public void NormalOrExactLaunchAndStrictFile_ApplyAllProcessGates(
        string? arguments,
        bool isDebugBuild)
    {
        var platform = FakePlatform.Valid();

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            arguments,
            isDebugBuild,
            "authorization.json",
            platform);

        Assert.True(result.IsApplied);
        Assert.Equal(DevelopmentLiveBootstrapStatus.Applied, result.Status);
        Assert.Equal("development-live-applied", result.Code);
        Assert.Equal(1, platform.ReadCount);
        Assert.Equal(
            NativeSha256.ToUpperInvariant(),
            platform.Environment[
                InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable]);
        Assert.Equal(
            InteropSha256,
            platform.Environment[
                InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable]);
        Assert.Equal(
            DevelopmentLiveLaunchBootstrap.RequiredMode,
            platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable]);
        Assert.Equal(
            PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
            platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable]);
    }

    [Theory]
    [MemberData(nameof(InvalidAuthorizationJson))]
    public void InvalidOrNonCanonicalAuthorization_IsRejectedWithoutEnvironmentMutation(
        string json)
    {
        var platform = new FakePlatform(Encoding.UTF8.GetBytes(json));

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            isDebugBuild: true,
            "authorization.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-authorization-invalid", result.Code);
        Assert.Empty(platform.Environment);
    }

    [Fact]
    public void OversizedAuthorization_IsRejectedBeforeParsing()
    {
        var platform = new FakePlatform(
            new byte[DevelopmentLiveLaunchBootstrap.MaximumAuthorizationFileBytes + 1]);

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            isDebugBuild: true,
            "authorization.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-authorization-invalid", result.Code);
        Assert.Empty(platform.Environment);
    }

    [Fact]
    public void PermissionFailure_IsFailClosedAndDoesNotLeakDetails()
    {
        var platform = FakePlatform.Valid();
        platform.ReadException = new UnauthorizedAccessException("sensitive local path");

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            isDebugBuild: true,
            "sensitive local path",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-authorization-unavailable", result.Code);
        Assert.DoesNotContain("sensitive", result.Code, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(platform.Environment);
    }

    [Fact]
    public void InvalidAuthorizationFileRead_IsUnavailableAndDoesNotMutateEnvironment()
    {
        var platform = FakePlatform.Valid();
        platform.ReadException = new InvalidDataException("sensitive authorization bytes");

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            null,
            isDebugBuild: false,
            "sensitive authorization path",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-authorization-unavailable", result.Code);
        Assert.False(result.IsLaunchAllowed);
        Assert.DoesNotContain("sensitive", result.Code, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(platform.Environment);
    }

    [Fact]
    public void EnvironmentFailure_DisarmsEveryPreviousProcessValue()
    {
        var platform = FakePlatform.Valid();
        platform.Environment[
            InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] = "old-native";
        platform.Environment[
            InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] = "old-interop";
        platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] =
            "stale-mode";
        platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
            "old-authorization";
        platform.FailOnceOnSetName = DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable;

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            isDebugBuild: true,
            "authorization.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Equal("development-live-environment-failed", result.Code);
        Assert.Empty(platform.Environment);
    }

    [Fact]
    public void RejectedExactDebugRequest_DisarmsInheritedLiveGates()
    {
        var platform = new FakePlatform(Encoding.UTF8.GetBytes("{}"));
        platform.Environment[
            InstalledK3dInteropMetadata.ApprovedNativeSha256EnvironmentVariable] =
            new string('A', 64);
        platform.Environment[
            InstalledK3dInteropMetadata.ApprovedSha256EnvironmentVariable] =
            new string('B', 64);
        platform.Environment[DevelopmentLiveLaunchBootstrap.ModeEnvironmentVariable] =
            DevelopmentLiveLaunchBootstrap.RequiredMode;
        platform.Environment[PlayoutOptionsLoader.LiveAuthorizationEnvironmentVariable] =
            PlayoutOptionsLoader.RequiredLiveAuthorizationValue;

        var result = DevelopmentLiveLaunchBootstrap.TryApply(
            DevelopmentLiveLaunchBootstrap.ExactLaunchArgument,
            isDebugBuild: true,
            "authorization.json",
            platform);

        Assert.Equal(DevelopmentLiveBootstrapStatus.Rejected, result.Status);
        Assert.Empty(platform.Environment);
    }

    public static TheoryData<string> InvalidAuthorizationJson() => new()
    {
        "{}",
        ValidJson().Replace("\"schemaVersion\": 1", "\"schemaVersion\": 2"),
        ValidJson().Replace("\"mode\": \"Live\"", "\"mode\": \"live\""),
        ValidJson().Replace(
            PlayoutOptionsLoader.RequiredLiveAuthorizationValue,
            "I_AUTHORIZE_SOMETHING_ELSE"),
        ValidJson().Replace(NativeSha256, new string('A', 63)),
        ValidJson().Replace(InteropSha256, new string('G', 64)),
        ValidJson().Replace(
            "\"interopSha256\"",
            "\"unexpected\": true, \"interopSha256\""),
        ValidJson().Replace(
            "\"mode\": \"Live\"",
            "\"mode\": \"Live\", \"mode\": \"Live\""),
        ValidJson().Replace("\"mode\"", "\"Mode\""),
        ValidJson().TrimEnd('}') + ",}",
        ValidJson().Replace("{", "{/* comment */", StringComparison.Ordinal),
        "[]",
        "null"
    };

    private static string ValidJson() =>
        $$"""
        {
          "schemaVersion": 1,
          "mode": "Live",
          "authorization": "{{PlayoutOptionsLoader.RequiredLiveAuthorizationValue}}",
          "nativeSha256": "{{NativeSha256}}",
          "interopSha256": "{{InteropSha256}}"
        }
        """;

    private sealed class FakePlatform(byte[] authorizationBytes) :
        IDevelopmentLiveBootstrapPlatform
    {
        public Dictionary<string, string?> Environment { get; } =
            new(StringComparer.Ordinal);

        public Exception? ReadException { get; set; }

        public string? FailOnceOnSetName { get; set; }

        public int ReadCount { get; private set; }

        public static FakePlatform Valid() =>
            new(Encoding.UTF8.GetBytes(ValidJson()));

        public byte[] ReadAuthorizationFile(string path, int maximumBytes)
        {
            ReadCount++;
            if (ReadException is not null)
            {
                throw ReadException;
            }

            return authorizationBytes;
        }

        public string? GetProcessEnvironmentVariable(string name) =>
            Environment.GetValueOrDefault(name);

        public void SetProcessEnvironmentVariable(string name, string? value)
        {
            if (string.Equals(name, FailOnceOnSetName, StringComparison.Ordinal))
            {
                FailOnceOnSetName = null;
                throw new SecurityException("simulated environment denial");
            }

            if (value is null)
            {
                Environment.Remove(name);
            }
            else
            {
                Environment[name] = value;
            }
        }
    }
}
