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

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class S5025TrustedManualFileDataSourceTests
{
    private static readonly Encoding Cp949 = CreateCp949Encoding();

    [Theory]
    [InlineData(S5025ManualAudience.Individual, "개인.dat")]
    [InlineData(S5025ManualAudience.Foreign, "외국인.dat")]
    [InlineData(S5025ManualAudience.Institution, "기관.dat")]
    public async Task ReadAsync_maps_closed_audience_to_fixed_cp949_file(
        S5025ManualAudience audience,
        string expectedFileName)
    {
        using var directory = new TemporaryDirectory();
        WriteValidFile(Path.Combine(directory.Path, expectedFileName));
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        var rows = await source.ReadAsync(audience);

        Assert.Equal(5, rows.Count);
        Assert.Equal("삼성전자", rows[0].LeftName);
        Assert.Equal("1,234", rows[0].LeftAmount);
        Assert.Equal("SK하이닉스", rows[0].RightName);
        Assert.Equal("-987", rows[0].RightAmount);
    }

    [Fact]
    public async Task CreateFromEnvironment_uses_only_documented_directory_setting()
    {
        using var directory = new TemporaryDirectory();
        WriteValidFile(Path.Combine(directory.Path, "개인.dat"));
        var requestedNames = new List<string>();
        var source = S5025TrustedManualFileDataSource.CreateFromEnvironment(name =>
        {
            requestedNames.Add(name);
            return directory.Path;
        });

        var rows = await source.ReadAsync(S5025ManualAudience.Individual);

        Assert.Equal(
            S5025TrustedManualFileDataSource.DirectoryEnvironmentVariable,
            Assert.Single(requestedNames));
        Assert.Equal(5, rows.Count);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("relative-data")]
    public void Constructor_rejects_missing_or_non_absolute_directory(string? path)
    {
        Assert.Throws<S5025TrustedManualDataException>(() =>
            new S5025TrustedManualFileDataSource(path!));
    }

    [Fact]
    public void Constructor_rejects_nonexistent_absolute_directory()
    {
        var missing = Path.Combine(
            Path.GetTempPath(),
            Guid.NewGuid().ToString("N"),
            "missing");

        Assert.Throws<S5025TrustedManualDataException>(() =>
            new S5025TrustedManualFileDataSource(missing));
    }

    [Theory]
    [InlineData(S5025ManualAudience.Individual)]
    [InlineData(S5025ManualAudience.Foreign)]
    [InlineData(S5025ManualAudience.Institution)]
    public async Task ReadAsync_does_not_fall_back_when_fixed_file_is_missing(
        S5025ManualAudience audience)
    {
        using var directory = new TemporaryDirectory();
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(audience));
    }

    [Fact]
    public async Task ReadAsync_rejects_unknown_audience_before_any_file_lookup()
    {
        using var directory = new TemporaryDirectory();
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync((S5025ManualAudience)99));
    }

    [Theory]
    [InlineData(
        "L1^1^R1^2\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2",
        "four rows")]
    [InlineData(
        "L1^1^R1^2\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2\r\nL6^1^R6^2",
        "six rows")]
    [InlineData(
        "L1^1^R1\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2",
        "three columns")]
    [InlineData(
        "L1^1^R1^2^EXTRA\r\nL2^1^R2^2\r\nL3^1^R3^2\r\nL4^1^R4^2\r\nL5^1^R5^2",
        "five columns")]
    public async Task ReadAsync_requires_exactly_five_rows_and_four_columns(
        string content,
        string _)
    {
        using var directory = new TemporaryDirectory();
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "개인.dat"),
            Cp949.GetBytes(content + "\r\n\r\n"));
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual));
    }

    [Fact]
    public async Task ReadAsync_rejects_missing_empty_terminal_record()
    {
        using var directory = new TemporaryDirectory();
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "개인.dat"),
            Cp949.GetBytes(ValidContent() + "\r\n"));
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual));
    }

    [Fact]
    public async Task ReadAsync_accepts_the_original_single_empty_terminal_record()
    {
        using var directory = new TemporaryDirectory();
        var content = ValidContent() + "\r\n\r\n";
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "개인.dat"),
            Cp949.GetBytes(content));
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        var rows = await source.ReadAsync(S5025ManualAudience.Individual);

        Assert.Equal(5, rows.Count);
    }

    [Fact]
    public async Task ReadAsync_rejects_multiple_empty_terminal_records()
    {
        using var directory = new TemporaryDirectory();
        var content = ValidContent() + "\r\n\r\n\r\n";
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "개인.dat"),
            Cp949.GetBytes(content));
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual));
    }

    [Fact]
    public async Task ReadAsync_rejects_invalid_cp949_and_unicode_bom_files()
    {
        using var invalidDirectory = new TemporaryDirectory();
        await File.WriteAllBytesAsync(
            Path.Combine(invalidDirectory.Path, "개인.dat"),
            [0x81]);
        var invalidSource = new S5025TrustedManualFileDataSource(invalidDirectory.Path);
        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            invalidSource.ReadAsync(S5025ManualAudience.Individual));

        using var bomDirectory = new TemporaryDirectory();
        var content = ValidContent();
        await File.WriteAllBytesAsync(
            Path.Combine(bomDirectory.Path, "개인.dat"),
            Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(content)).ToArray());
        var bomSource = new S5025TrustedManualFileDataSource(bomDirectory.Path);
        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            bomSource.ReadAsync(S5025ManualAudience.Individual));
    }

    [Fact]
    public async Task ReadAsync_rejects_oversized_file_before_decoding()
    {
        using var directory = new TemporaryDirectory();
        await File.WriteAllBytesAsync(
            Path.Combine(directory.Path, "개인.dat"),
            new byte[S5025TrustedManualFileDataSource.MaximumFileSizeBytes + 1]);
        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual));
    }

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

        var source = new S5025TrustedManualFileDataSource(directory.Path);

        await Assert.ThrowsAsync<S5025TrustedManualDataException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual));
    }

    [Fact]
    public void Constructor_rejects_reparse_directory_or_ancestor()
    {
        using var parent = new TemporaryDirectory();
        using var outside = new TemporaryDirectory();
        var link = Path.Combine(parent.Path, "linked-data");
        if (!TryCreateDirectorySymbolicLink(link, outside.Path))
        {
            return;
        }

        Assert.Throws<S5025TrustedManualDataException>(() =>
            new S5025TrustedManualFileDataSource(link));
    }

    [Fact]
    public async Task ReadAsync_honors_pre_cancelled_token()
    {
        using var directory = new TemporaryDirectory();
        WriteValidFile(Path.Combine(directory.Path, "개인.dat"));
        var source = new S5025TrustedManualFileDataSource(directory.Path);
        using var cancellation = new CancellationTokenSource();
        cancellation.Cancel();

        await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
            source.ReadAsync(S5025ManualAudience.Individual, cancellation.Token));
    }

    private static void WriteValidFile(string path)
    {
        var content = ValidContent() + "\r\n\r\n";
        File.WriteAllBytes(path, Cp949.GetBytes(content));
    }

    private static string ValidContent() => string.Join(
        "\r\n",
        "삼성전자^1,234^SK하이닉스^-987",
        "현대차^2^기아^-2",
        "NAVER^3^카카오^-3",
        "LG화학^4^셀트리온^-4",
        "POSCO홀딩스^5^한화에어로스페이스^-5");

    private static Encoding CreateCp949Encoding()
    {
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
        return Encoding.GetEncoding(
            949,
            EncoderFallback.ExceptionFallback,
            DecoderFallback.ExceptionFallback);
    }

    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 static bool TryCreateDirectorySymbolicLink(string link, string target)
    {
        try
        {
            Directory.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 (IOException)
            {
                // A failed cleanup must not hide the behavior under test.
            }
            catch (UnauthorizedAccessException)
            {
                // A failed cleanup must not hide the behavior under test.
            }
        }
    }
}
