using System.Text;
using MBN_STOCK_WEBVIEW.Infrastructure;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class ViTrustedManualFileStoreTests
{
    [Fact]
    public async Task ReadAsync_imports_the_original_nine_column_cp949_shape()
    {
        using var directory = new TemporaryDirectory();
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        var cp949 = Encoding.GetEncoding(949);
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "VI발동.dat"),
            cp949.GetBytes(
                "PKR7005930003^삼성전자^^^^^^^\r\n" +
                "PKR7016360000^삼성증권^^^^^^^\r\n"));
        using var store = CreateStore(directory.Path);

        var actual = await store.ReadAsync();

        Assert.Equal(
            [
                new ViTrustedManualItem("PKR7005930003", "삼성전자"),
                new ViTrustedManualItem("PKR7016360000", "삼성증권")
            ],
            actual);
    }

    [Fact]
    public async Task WriteAsync_round_trips_order_duplicates_and_version()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        ViTrustedManualItem[] expected =
        [
            new("005930", "삼성전자"),
            new("016360", "삼성증권"),
            new("005930", "삼성전자")
        ];

        var write = await store.WriteAsync(expected);

        Assert.True(write.Persisted);
        Assert.Matches("^[a-f0-9]{64}$", write.Snapshot.RowVersion);
        Assert.Equal(expected, await store.ReadAsync());
        Assert.Empty(Directory.EnumerateFiles(directory.Path, "*.tmp"));
    }

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

        var firstNoOp = await store.WriteAsync(Array.Empty<ViTrustedManualItem>());
        Assert.False(firstNoOp.Persisted);
        Assert.False(File.Exists(Path.Combine(directory.Path, "VI발동.dat")));
        Assert.Empty(await store.ReadAsync());

        var expected = new[] { new ViTrustedManualItem("005930", "삼성전자") };
        var written = await store.WriteAsync(expected);
        var secondNoOp = await store.WriteAsync(Array.Empty<ViTrustedManualItem>());

        Assert.False(secondNoOp.Persisted);
        Assert.Equal(written.Snapshot.RowVersion, secondNoOp.Snapshot.RowVersion);
        Assert.Equal(expected, await store.ReadAsync());
    }

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

        var snapshot = await store.ReadSnapshotAsync();

        Assert.Empty(snapshot.Items);
        Assert.Equal(
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
            snapshot.RowVersion);
    }

    [Fact]
    public async Task ReadAsync_rejects_nonempty_legacy_extra_columns()
    {
        using var directory = new TemporaryDirectory();
        await File.WriteAllTextAsync(
            Path.Combine(directory.Path, "VI발동.dat"),
            "005930^삼성전자^unexpected^^^^^^\r\n");
        using var store = CreateStore(directory.Path);

        await Assert.ThrowsAsync<ViTrustedManualDataException>(() => store.ReadAsync());
    }

    [Theory]
    [InlineData("", "삼성전자")]
    [InlineData("005930", "")]
    [InlineData("005^930", "삼성전자")]
    [InlineData("005930", "삼성\n전자")]
    [InlineData(" 005930", "삼성전자")]
    [InlineData("005930", "삼성전자 ")]
    [InlineData("005930", "삼성,전자")]
    [InlineData("005930", "😀")]
    public async Task WriteAsync_rejects_invalid_or_non_cp949_values(string code, string name)
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);

        await Assert.ThrowsAsync<ViTrustedManualDataException>(() =>
            store.WriteAsync([new ViTrustedManualItem(code, name)]));
    }

    [Fact]
    public async Task WriteAsync_caps_the_original_five_row_scene_at_twenty_pages()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        var items = Enumerable.Range(1, ViTrustedManualFileStore.MaximumItemCount + 1)
            .Select(index => new ViTrustedManualItem(index.ToString("D6"), $"종목{index}"))
            .ToArray();

        await Assert.ThrowsAsync<ViTrustedManualDataException>(() => store.WriteAsync(items));
    }

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

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            store.WriteAsync(
                [new ViTrustedManualItem("005930", "삼성전자")],
                cancellation.Token));
        Assert.Empty(Directory.EnumerateFiles(directory.Path));
    }

    [Fact]
    public async Task Canonical_snapshot_version_covers_all_twenty_pages()
    {
        using var directory = new TemporaryDirectory();
        using var store = CreateStore(directory.Path);
        var items = Enumerable.Range(1, ViTrustedManualFileStore.MaximumItemCount)
            .Select(index => new ViTrustedManualItem(
                $"P{index:D6}",
                $"현실적인긴종목명테스트{index}"))
            .ToArray();

        var written = await store.WriteAsync(items);
        var snapshot = await store.ReadSnapshotAsync();

        Assert.True(written.Persisted);
        Assert.Equal(100, snapshot.Items.Count);
        Assert.Equal(written.Snapshot.RowVersion, snapshot.RowVersion);
        Assert.Matches("^[a-f0-9]{64}$", snapshot.RowVersion);
    }

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

    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.
            }
        }
    }
}
