using System.Reflection;
using MBN_STOCK_WEBVIEW.Infrastructure.Diagnostics;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class NativeOperatorLogWriterTests
{
    private static readonly DateTimeOffset FixedUtcNow =
        new(2026, 7, 12, 3, 4, 5, 678, TimeSpan.Zero);

    [Fact]
    public void TryWrite_WritesOnlyClosedEventsToTheFixedLocalApplicationDataPath()
    {
        using var scope = TemporaryDirectory.Create();
        var writer = CreateWriter(scope.Path);
        var records = new[]
        {
            NativeOperatorLogRecord.ForStartup(),
            NativeOperatorLogRecord.ForShutdown(),
            NativeOperatorLogRecord.ForOracleStateChanged(NativeOperatorDatabaseState.Healthy),
            NativeOperatorLogRecord.ForMariaDbStateChanged(NativeOperatorDatabaseState.Unhealthy),
            NativeOperatorLogRecord.ForTornadoStateChanged(NativeOperatorTornadoState.Connected),
            NativeOperatorLogRecord.ForPlayoutCommandRequested(NativeOperatorPlayoutCommand.Prepare),
            NativeOperatorLogRecord.ForPlayoutCommandCompleted(
                NativeOperatorPlayoutCommand.Prepare,
                NativeOperatorPlayoutCompletion.Succeeded),
            NativeOperatorLogRecord.ForOutcomeUnknown(NativeOperatorPlayoutCommand.TakeIn)
        };

        Assert.All(records, record => Assert.True(writer.TryWrite(record)));

        var expectedPath = Path.Combine(
            scope.Path,
            "MBN_STOCK_WEBVIEW",
            "Logs",
            "Log_0712.log");
        Assert.True(File.Exists(expectedPath));
        Assert.Equal(
            new[]
            {
                "[2026-07-12T03:04:05.678+00:00] event=Startup",
                "[2026-07-12T03:04:05.678+00:00] event=Shutdown",
                "[2026-07-12T03:04:05.678+00:00] event=OracleStateChanged state=Healthy",
                "[2026-07-12T03:04:05.678+00:00] event=MariaDbStateChanged state=Unhealthy",
                "[2026-07-12T03:04:05.678+00:00] event=TornadoStateChanged state=Connected",
                "[2026-07-12T03:04:05.678+00:00] event=PlayoutCommandRequested command=Prepare",
                "[2026-07-12T03:04:05.678+00:00] event=PlayoutCommandCompleted command=Prepare result=Succeeded",
                "[2026-07-12T03:04:05.678+00:00] event=OutcomeUnknown command=TakeIn"
            },
            File.ReadAllLines(expectedPath));
    }

    [Fact]
    public void PublicApi_CannotAcceptFreeFormTextPathsSqlExceptionsOrSceneData()
    {
        Assert.Equal(
            new[]
            {
                "Startup",
                "Shutdown",
                "OracleStateChanged",
                "MariaDbStateChanged",
                "TornadoStateChanged",
                "PlayoutCommandRequested",
                "PlayoutCommandCompleted",
                "OutcomeUnknown"
            },
            Enum.GetNames<NativeOperatorLogEvent>());

        var recordType = typeof(NativeOperatorLogRecord);
        Assert.All(
            recordType.GetProperties(BindingFlags.Instance | BindingFlags.Public),
            property =>
            {
                var scalarType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
                Assert.True(scalarType.IsEnum, $"{property.Name} must remain a closed enum scalar.");
            });
        Assert.DoesNotContain(
            recordType.GetConstructors(BindingFlags.Instance | BindingFlags.Public),
            constructor => constructor.GetParameters().Length > 0);

        var factories = recordType.GetMethods(BindingFlags.Static | BindingFlags.Public)
            .Where(method => method.Name.StartsWith("For", StringComparison.Ordinal))
            .ToArray();
        Assert.Equal(8, factories.Length);
        Assert.All(
            factories.SelectMany(factory => factory.GetParameters()),
            parameter => Assert.True(parameter.ParameterType.IsEnum));

        var publicWriterConstructors = typeof(NativeOperatorLogWriter)
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public);
        var constructor = Assert.Single(publicWriterConstructors);
        Assert.Empty(constructor.GetParameters());
        var tryWrite = typeof(INativeOperatorLogWriter).GetMethod(nameof(INativeOperatorLogWriter.TryWrite));
        Assert.NotNull(tryWrite);
        Assert.Equal(
            new[] { typeof(NativeOperatorLogRecord) },
            tryWrite.GetParameters().Select(parameter => parameter.ParameterType));
    }

    [Fact]
    public void TryWrite_InvalidDefaultAndUndefinedEnumsFailClosedWithoutCreatingALog()
    {
        using var scope = TemporaryDirectory.Create();
        var writer = CreateWriter(scope.Path);

        Assert.False(writer.TryWrite(default));
        Assert.False(writer.TryWrite(NativeOperatorLogRecord.ForOracleStateChanged(
            (NativeOperatorDatabaseState)int.MaxValue)));
        Assert.False(writer.TryWrite(NativeOperatorLogRecord.ForPlayoutCommandCompleted(
            NativeOperatorPlayoutCommand.Prepare,
            (NativeOperatorPlayoutCompletion)int.MaxValue)));

        Assert.False(Directory.Exists(Path.Combine(scope.Path, "MBN_STOCK_WEBVIEW")));
    }

    [Fact]
    public void TryWrite_AcceptsEveryDefinedClosedScalarAndNoUnmappedRuntimeState()
    {
        using var scope = TemporaryDirectory.Create();
        var writer = CreateWriter(scope.Path);

        foreach (var state in Enum.GetValues<NativeOperatorDatabaseState>())
        {
            Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForOracleStateChanged(state)));
            Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForMariaDbStateChanged(state)));
        }

        foreach (var state in Enum.GetValues<NativeOperatorTornadoState>())
        {
            Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForTornadoStateChanged(state)));
        }

        foreach (var command in Enum.GetValues<NativeOperatorPlayoutCommand>())
        {
            Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForPlayoutCommandRequested(command)));
            Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForOutcomeUnknown(command)));
            foreach (var completion in Enum.GetValues<NativeOperatorPlayoutCompletion>())
            {
                Assert.True(writer.TryWrite(
                    NativeOperatorLogRecord.ForPlayoutCommandCompleted(command, completion)));
            }
        }
    }

    [Fact]
    public void TryWrite_StopsAtTheConfiguredFileSizeWithoutChangingExistingBytes()
    {
        using var scope = TemporaryDirectory.Create();
        var initialWriter = CreateWriter(scope.Path);
        Assert.True(initialWriter.TryWrite(NativeOperatorLogRecord.ForStartup()));
        var path = LogPath(scope.Path);
        var original = File.ReadAllBytes(path);

        var boundedWriter = CreateWriter(scope.Path, maximumFileBytes: original.Length);
        Assert.False(boundedWriter.TryWrite(NativeOperatorLogRecord.ForShutdown()));

        Assert.Equal(original, File.ReadAllBytes(path));
    }

    [Fact]
    public void TryWrite_DeletesOnlyExpiredClosedLogNamesAndKeepsRecentOrUnrelatedFiles()
    {
        using var scope = TemporaryDirectory.Create();
        var logDirectory = Path.Combine(scope.Path, "MBN_STOCK_WEBVIEW", "Logs");
        Directory.CreateDirectory(logDirectory);
        var expired = Path.Combine(logDirectory, "Log_0601.log");
        var recent = Path.Combine(logDirectory, "Log_0711.log");
        var unrelated = Path.Combine(logDirectory, "operator-notes.txt");
        File.WriteAllText(expired, "expired");
        File.WriteAllText(recent, "recent");
        File.WriteAllText(unrelated, "unrelated");
        File.SetLastWriteTimeUtc(expired, FixedUtcNow.UtcDateTime.AddDays(-12));
        File.SetLastWriteTimeUtc(recent, FixedUtcNow.UtcDateTime.AddDays(-1));
        File.SetLastWriteTimeUtc(unrelated, FixedUtcNow.UtcDateTime.AddDays(-100));
        var writer = CreateWriter(scope.Path, retentionDays: 10);

        Assert.True(writer.TryWrite(NativeOperatorLogRecord.ForStartup()));

        Assert.False(File.Exists(expired));
        Assert.True(File.Exists(recent));
        Assert.True(File.Exists(unrelated));
        Assert.True(File.Exists(LogPath(scope.Path)));
    }

    [Fact]
    public async Task TryWrite_ConcurrentWritersProduceCompleteNonInterleavedLines()
    {
        using var scope = TemporaryDirectory.Create();
        var writers = Enumerable.Range(0, 4)
            .Select(_ => CreateWriter(scope.Path))
            .ToArray();
        const int taskCount = 8;
        const int writesPerTask = 40;

        var tasks = Enumerable.Range(0, taskCount)
            .Select(taskIndex => Task.Run(() =>
            {
                var writer = writers[taskIndex % writers.Length];
                for (var index = 0; index < writesPerTask; index++)
                {
                    if (!writer.TryWrite(NativeOperatorLogRecord.ForTornadoStateChanged(
                            NativeOperatorTornadoState.Connected)))
                    {
                        return false;
                    }
                }

                return true;
            }))
            .ToArray();

        var results = await Task.WhenAll(tasks);

        Assert.All(results, Assert.True);
        var lines = File.ReadAllLines(LogPath(scope.Path));
        Assert.Equal(taskCount * writesPerTask, lines.Length);
        Assert.All(
            lines,
            line => Assert.Equal(
                "[2026-07-12T03:04:05.678+00:00] event=TornadoStateChanged state=Connected",
                line));
    }

    [Fact]
    public void TryWrite_UnsafeDirectoryLayoutsAndReparseAttributesFailWithoutThrowing()
    {
        using var scope = TemporaryDirectory.Create();
        File.WriteAllText(Path.Combine(scope.Path, "MBN_STOCK_WEBVIEW"), "not a directory");
        var writer = CreateWriter(scope.Path);

        Assert.False(writer.TryWrite(NativeOperatorLogRecord.ForStartup()));
        Assert.False(NativeOperatorLogWriter.IsSafeDirectoryAttributes(
            FileAttributes.Directory | FileAttributes.ReparsePoint));
        Assert.False(NativeOperatorLogWriter.IsSafeFileAttributes(
            FileAttributes.Normal | FileAttributes.ReparsePoint));
        Assert.False(NativeOperatorLogWriter.IsSafeFileAttributes(FileAttributes.Directory));
        Assert.True(NativeOperatorLogWriter.IsSafeDirectoryAttributes(FileAttributes.Directory));
        Assert.True(NativeOperatorLogWriter.IsSafeFileAttributes(FileAttributes.Normal));
    }

    [Fact]
    public void TryWrite_MalformedInternalRootIsAlwaysBestEffort()
    {
        var writer = new NativeOperatorLogWriter(
            "relative-root",
            new FixedTimeProvider(FixedUtcNow),
            NativeOperatorLogWriter.DefaultMaximumFileBytes,
            NativeOperatorLogWriter.DefaultRetentionDays);

        var exception = Record.Exception(() =>
            Assert.False(writer.TryWrite(NativeOperatorLogRecord.ForStartup())));

        Assert.Null(exception);
    }

    private static NativeOperatorLogWriter CreateWriter(
        string localApplicationDataRoot,
        int maximumFileBytes = NativeOperatorLogWriter.DefaultMaximumFileBytes,
        int retentionDays = NativeOperatorLogWriter.DefaultRetentionDays) =>
        new(
            localApplicationDataRoot,
            new FixedTimeProvider(FixedUtcNow),
            maximumFileBytes,
            retentionDays);

    private static string LogPath(string localApplicationDataRoot) =>
        Path.Combine(localApplicationDataRoot, "MBN_STOCK_WEBVIEW", "Logs", "Log_0712.log");

    private sealed class FixedTimeProvider(DateTimeOffset utcNow) : TimeProvider
    {
        public override DateTimeOffset GetUtcNow() => utcNow;

        public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Utc;
    }

    private sealed class TemporaryDirectory : IDisposable
    {
        private TemporaryDirectory(string path)
        {
            Path = path;
        }

        public string Path { get; }

        public static TemporaryDirectory Create()
        {
            var path = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                $"MBN_STOCK_WEBVIEW-log-tests-{Guid.NewGuid():N}");
            Directory.CreateDirectory(path);
            return new TemporaryDirectory(path);
        }

        public void Dispose()
        {
            try
            {
                Directory.Delete(Path, recursive: true);
            }
            catch
            {
                // Test cleanup is best effort.
            }
        }
    }
}
