using System.Text;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class S5025TrustedManualFileStoreTests
{
    [Theory]
    [InlineData(S5025ManualAudience.Individual, "개인.dat")]
    [InlineData(S5025ManualAudience.Foreign, "외국인.dat")]
    [InlineData(S5025ManualAudience.Institution, "기관.dat")]
    public async Task WriteAsync_round_trips_the_closed_audience_file(
        S5025ManualAudience audience,
        string expectedFileName)
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);

        await store.WriteAsync(audience, Rows());

        Assert.True(File.Exists(Path.Combine(directory.Path, expectedFileName)));
        var actual = await store.ReadAsync(audience);
        Assert.Equal(Rows(), actual);
        Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.tmp"));
    }

    [Fact]
    public async Task WriteAsync_preserves_the_legacy_cp949_terminal_record()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);

        await store.WriteAsync(S5025ManualAudience.Individual, Rows());

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        var cp949 = Encoding.GetEncoding(949);
        var bytes = await File.ReadAllBytesAsync(Path.Combine(directory.Path, "개인.dat"));
        var text = cp949.GetString(bytes);
        Assert.EndsWith("\r\n\r\n", text, StringComparison.Ordinal);
        Assert.Equal(5, text.Split("\r\n", StringSplitOptions.None).Length - 2);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(4)]
    [InlineData(6)]
    public async Task WriteAsync_requires_exactly_five_rows(int rowCount)
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            store.WriteAsync(
                S5025ManualAudience.Individual,
                Rows().Take(rowCount).Concat(Enumerable.Repeat(Rows()[0], Math.Max(0, rowCount - 5)))
                    .Take(rowCount)
                    .ToArray()));
    }

    [Theory]
    [InlineData("bad^name")]
    [InlineData("bad\nname")]
    public async Task WriteAsync_rejects_delimiter_and_control_characters(string value)
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        var rows = Rows().ToArray();
        rows[0] = rows[0] with { LeftName = value };

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            store.WriteAsync(S5025ManualAudience.Individual, rows));
    }

    [Fact]
    public async Task WriteAsync_rejects_text_that_cp949_cannot_encode()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        var rows = Rows().ToArray();
        rows[0] = rows[0] with { LeftName = "😀" };

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            store.WriteAsync(S5025ManualAudience.Individual, rows));
    }

    [Fact]
    public async Task WriteAsync_honors_pre_cancelled_token_without_creating_a_file()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            store.WriteAsync(S5025ManualAudience.Individual, Rows(), cancellation.Token));
        Assert.Empty(Directory.EnumerateFiles(directory.Path));
    }

    [Fact]
    public async Task WriteAsync_rejects_unknown_audience_before_creating_a_file()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            store.WriteAsync((S5025ManualAudience)99, Rows()));
        Assert.Empty(Directory.EnumerateFiles(directory.Path));
    }

    [Fact]
    public async Task WriteAsync_rejects_an_existing_reparse_destination()
    {
        using var directory = new TemporaryDirectory();
        using var outside = new TemporaryDirectory();
        var target = Path.Combine(outside.Path, "outside.dat");
        await File.WriteAllTextAsync(target, "outside");
        var link = Path.Combine(directory.Path, "개인.dat");
        if (!TryCreateFileSymbolicLink(link, target))
        {
            return;
        }

        using var store = CreateStore(directory.Path);
        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            store.WriteAsync(S5025ManualAudience.Individual, Rows()));
        Assert.Equal("outside", await File.ReadAllTextAsync(target));
    }

    private static IReadOnlyList<S5025TrustedManualRow> Rows() =>
    [
        new("삼성전자", "1,234", "SK하이닉스", "-987"),
        new("현대차", "2", "기아", "-2"),
        new("NAVER", "3", "카카오", "-3"),
        new("LG화학", "4", "포스코", "-4"),
        new("한화", "5", "셀트리온", "-5")
    ];

    private static S5025TrustedManualFileStore CreateStore(string directory)
    {
        TrustedManualDirectory.PreparePrivateWritableDirectory(directory);
        return new S5025TrustedManualFileStore(directory);
    }

    private static bool TryCreateFileSymbolicLink(string link, string target)
    {
        try
        {
            File.CreateSymbolicLink(link, target);
            return true;
        }
        catch (Exception exception) when (exception is
            UnauthorizedAccessException or IOException or PlatformNotSupportedException)
        {
            return false;
        }
    }

    private sealed class TemporaryDirectory : IDisposable
    {
        public TemporaryDirectory()
        {
            Path = System.IO.Path.Combine(
                System.IO.Path.GetTempPath(),
                Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(Path);
        }

        public string Path { get; }

        public void Dispose()
        {
            try
            {
                Directory.Delete(Path, recursive: true);
            }
            catch (Exception exception) when (exception is
                IOException or UnauthorizedAccessException)
            {
                // Cleanup must not hide the test result.
            }
        }
    }
}
