using System.Text.Json;
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
using MBN_STOCK_WEBVIEW.Infrastructure;
using MMoneyCoderSharp.Data;

namespace MBN_STOCK_WEBVIEW.Infrastructure.Tests;

public sealed class LegacyManualOperatorDataImporterTests
{
    private static readonly string[] DestinationFiles =
    [
        "개인.dat",
        "외국인.dat",
        "기관.dat",
        "VI발동.dat"
    ];

    [Fact]
    public async Task Fixed_source_reads_the_four_original_files_without_modifying_them()
    {
        using var sourceDirectory = new TemporaryDirectory();
        WriteSourceFiles(sourceDirectory.Path);
        var before = DestinationFiles.ToDictionary(
            name => name,
            name => File.ReadAllBytes(Path.Combine(sourceDirectory.Path, name)));
        var source = new FixedLegacyManualOperatorDataSource(
            sourceDirectory.Path,
            TimeSpan.Zero);

        var snapshot = await source.ReadAsync();

        Assert.Equal(5, snapshot.IndividualRows.Count);
        Assert.Equal(5, snapshot.ForeignRows.Count);
        Assert.Equal(5, snapshot.InstitutionRows.Count);
        Assert.Equal(3, snapshot.ViItems.Count);
        Assert.Matches("^[a-f0-9]{64}$", snapshot.SourceSha256);
        Assert.All(DestinationFiles, name =>
            Assert.Equal(before[name], File.ReadAllBytes(Path.Combine(sourceDirectory.Path, name))));
    }

    [Fact]
    public async Task Import_commits_all_four_files_and_the_versioned_marker_last()
    {
        using var sourceDirectory = new TemporaryDirectory();
        using var destinationDirectory = new TemporaryDirectory();
        WriteSourceFiles(sourceDirectory.Path);
        PreparePrivate(destinationDirectory.Path);
        var now = new DateTimeOffset(2026, 7, 12, 1, 2, 3, TimeSpan.Zero);
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            new FixedLegacyManualOperatorDataSource(sourceDirectory.Path, TimeSpan.Zero),
            () => now);

        var result = await importer.ImportAsync();

        Assert.Equal(1, result.MarkerVersion);
        Assert.Equal(15, result.NetSellRowCount);
        Assert.Equal(3, result.ViItemCount);
        Assert.Equal(now, result.ImportedAt);
        Assert.All(DestinationFiles, name =>
            Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, name))));
        var markerPath = Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.MarkerFileName);
        Assert.True(File.Exists(markerPath));
        using var marker = JsonDocument.Parse(await File.ReadAllBytesAsync(markerPath));
        Assert.Equal(1, marker.RootElement.GetProperty("schemaVersion").GetInt32());
        Assert.Equal(result.SourceSha256, marker.RootElement.GetProperty("sourceSha256").GetString());
        Assert.Matches(
            "^[a-f0-9]{64}$",
            marker.RootElement.GetProperty("productionSha256").GetString()!);
        Assert.Equal(15, marker.RootElement.GetProperty("netSellRowCount").GetInt32());
        Assert.Equal(3, marker.RootElement.GetProperty("viItemCount").GetInt32());
        Assert.False(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName)));
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.ReadyImportedData,
            LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);

        using var netStore = new S5025TrustedManualFileStore(destinationDirectory.Path);
        using var viStore = new ViTrustedManualFileStore(destinationDirectory.Path);
        Assert.Equal(Rows(), await netStore.ReadAsync(S5025ManualAudience.Individual));
        Assert.Equal(3, (await viStore.ReadAsync()).Count);

        var serializedResult = JsonSerializer.Serialize(result);
        var serializedMarker = await File.ReadAllTextAsync(markerPath);
        Assert.DoesNotContain(sourceDirectory.Path, serializedResult, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain(sourceDirectory.Path, serializedMarker, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("source\\repos", serializedMarker, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Empty_vi_file_keeps_its_distinct_zero_length_contract_after_import_and_restart()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var snapshot = new LegacyManualOperatorSourceSnapshot(
            Rows(),
            Rows(),
            Rows(),
            Array.Empty<LegacyManualViItem>(),
            new string('c', 64));
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            new CountingSource(snapshot));

        var result = await importer.ImportAsync();

        Assert.Equal(0, result.ViItemCount);
        Assert.Equal(
            0,
            new FileInfo(Path.Combine(destinationDirectory.Path, "VI발동.dat")).Length);
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.ReadyImportedData,
            LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
        using var viStore = new ViTrustedManualFileStore(destinationDirectory.Path);
        Assert.Empty(await viStore.ReadAsync());
    }

    [Theory]
    [InlineData("개인.dat")]
    [InlineData("외국인.dat")]
    [InlineData("기관.dat")]
    [InlineData("VI발동.dat")]
    public async Task Any_existing_private_data_file_blocks_the_whole_import_without_overwrite(
        string existingFile)
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var path = Path.Combine(destinationDirectory.Path, existingFile);
        var original = new byte[] { 1, 2, 3, 4 };
        await File.WriteAllBytesAsync(path, original);
        var source = new CountingSource(Snapshot());
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            source);

        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.DestinationNotEmpty, error.Failure);
        Assert.False(error.OutcomeUnknown);
        Assert.Equal(0, source.ReadCount);
        Assert.Equal(original, await File.ReadAllBytesAsync(path));
        Assert.Single(Directory.EnumerateFileSystemEntries(destinationDirectory.Path));
    }

    [Fact]
    public async Task Marker_blocks_a_second_import_before_the_source_is_read_again()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var source = new CountingSource(Snapshot());
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            source);

        _ = await importer.ImportAsync();
        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.AlreadyImported, error.Failure);
        Assert.False(error.OutcomeUnknown);
        Assert.Equal(1, source.ReadCount);
    }

    [Fact]
    public async Task Preexisting_intent_and_partial_data_fail_closed_before_source_or_store_use()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var intentPath = Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName);
        await File.WriteAllTextAsync(intentPath, "{}");
        var partialPath = Path.Combine(destinationDirectory.Path, DestinationFiles[0]);
        var partialBytes = LegacyManualOperatorDataCodec.SerializeNetSell(Rows());
        await File.WriteAllBytesAsync(partialPath, partialBytes);

        var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
            destinationDirectory.Path);
        var source = new CountingSource(Snapshot());
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            source);
        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorDataRuntimeState.InterruptedImport, inspection.State);
        Assert.False(inspection.IsReady);
        Assert.Equal(LegacyManualOperatorImportFailure.InterruptedImport, error.Failure);
        Assert.True(error.OutcomeUnknown);
        Assert.Equal(0, source.ReadCount);
        Assert.Equal(partialBytes, await File.ReadAllBytesAsync(partialPath));
        Assert.Equal("{}", await File.ReadAllTextAsync(intentPath));
    }

    [Fact]
    public async Task Existing_legacy_files_without_marker_or_intent_remain_runtime_eligible()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        await File.WriteAllBytesAsync(
            Path.Combine(destinationDirectory.Path, DestinationFiles[0]),
            LegacyManualOperatorDataCodec.SerializeNetSell(Rows()));

        var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
            destinationDirectory.Path);

        Assert.True(inspection.IsReady);
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.ReadyLegacyData,
            inspection.State);
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(4)]
    public async Task Failure_after_each_data_move_rolls_back_data_and_intent(
        int failAfterMove)
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            new CountingSource(Snapshot()),
            utcNow: null,
            afterDataFileMoved: movedCount =>
            {
                if (movedCount == failAfterMove)
                {
                    throw new IOException("injected move failure");
                }
            });

        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.ImportFailed, error.Failure);
        Assert.False(error.OutcomeUnknown);
        Assert.All(DestinationFiles, name =>
            Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, name))));
        Assert.False(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.MarkerFileName)));
        Assert.False(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName)));
    }

    [Theory]
    [InlineData(1)]
    [InlineData(2)]
    [InlineData(3)]
    [InlineData(4)]
    public async Task Abrupt_termination_after_each_move_leaves_intent_and_blocks_all_reuse(
        int crashAfterMove)
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        using (var importer = new LegacyManualOperatorDataImporter(
                   destinationDirectory.Path,
                   new CountingSource(Snapshot()),
                   utcNow: null,
                   afterDataFileMoved: movedCount =>
                   {
                       if (movedCount == crashAfterMove)
                       {
                           throw new LegacyManualOperatorImportCrashSimulationException();
                       }
                   }))
        {
            await Assert.ThrowsAsync<LegacyManualOperatorImportCrashSimulationException>(
                () => importer.ImportAsync());
        }

        Assert.True(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName)));
        Assert.Equal(
            crashAfterMove,
            DestinationFiles.Count(name => File.Exists(Path.Combine(
                destinationDirectory.Path,
                name))));
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.InterruptedImport,
            LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);

        var secondSource = new CountingSource(Snapshot());
        using var secondImporter = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            secondSource);
        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => secondImporter.ImportAsync());
        Assert.Equal(LegacyManualOperatorImportFailure.InterruptedImport, error.Failure);
        Assert.True(error.OutcomeUnknown);
        Assert.Equal(0, secondSource.ReadCount);
    }

    [Fact]
    public async Task Abrupt_termination_after_completed_marker_keeps_stale_intent_fail_closed()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        using (var importer = new LegacyManualOperatorDataImporter(
                   destinationDirectory.Path,
                   new CountingSource(Snapshot()),
                   utcNow: null,
                   afterDataFileMoved: null,
                   afterMarkerCommitted: () =>
                       throw new LegacyManualOperatorImportCrashSimulationException()))
        {
            await Assert.ThrowsAsync<LegacyManualOperatorImportCrashSimulationException>(
                () => importer.ImportAsync());
        }

        Assert.All(DestinationFiles, name =>
            Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, name))));
        Assert.True(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.MarkerFileName)));
        Assert.True(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName)));
        var inspection = LegacyManualOperatorDataRuntimeGuard.Inspect(
            destinationDirectory.Path);
        Assert.False(inspection.IsReady);
        Assert.Equal(LegacyManualOperatorDataRuntimeState.InterruptedImport, inspection.State);
    }

    [Fact]
    public async Task Completed_marker_with_missing_or_modified_data_is_rejected()
    {
        using var missingDirectory = new TemporaryDirectory();
        PreparePrivate(missingDirectory.Path);
        using (var importer = new LegacyManualOperatorDataImporter(
                   missingDirectory.Path,
                   new CountingSource(Snapshot())))
        {
            _ = await importer.ImportAsync();
        }
        File.Delete(Path.Combine(missingDirectory.Path, DestinationFiles[0]));
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.InvalidImportedState,
            LegacyManualOperatorDataRuntimeGuard.Inspect(missingDirectory.Path).State);

        using var modifiedDirectory = new TemporaryDirectory();
        PreparePrivate(modifiedDirectory.Path);
        using (var importer = new LegacyManualOperatorDataImporter(
                   modifiedDirectory.Path,
                   new CountingSource(Snapshot())))
        {
            _ = await importer.ImportAsync();
        }
        await File.AppendAllTextAsync(
            Path.Combine(modifiedDirectory.Path, DestinationFiles[0]),
            "tamper");
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.InvalidImportedState,
            LegacyManualOperatorDataRuntimeGuard.Inspect(modifiedDirectory.Path).State);
    }

    [Fact]
    public async Task Completed_marker_allows_later_verified_operator_edits()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        using (var importer = new LegacyManualOperatorDataImporter(
                   destinationDirectory.Path,
                   new CountingSource(Snapshot())))
        {
            _ = await importer.ImportAsync();
        }

        var editedRows = Rows().ToArray();
        editedRows[0] = editedRows[0] with { LeftAmount = "9,999" };
        using (var store = new S5025TrustedManualFileStore(destinationDirectory.Path))
        {
            await store.WriteAsync(S5025ManualAudience.Individual, editedRows);
        }

        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.ReadyImportedData,
            LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
    }

    [Fact]
    public async Task Commit_failure_rolls_back_every_file_created_by_the_operation_without_retry()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var source = new CountingSource(Snapshot());
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            source,
            utcNow: null,
            afterDataFileMoved: movedCount =>
            {
                if (movedCount == 1)
                {
                    Directory.CreateDirectory(Path.Combine(destinationDirectory.Path, "외국인.dat"));
                }
            });

        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.ImportFailed, error.Failure);
        Assert.False(error.OutcomeUnknown);
        Assert.Equal(1, source.ReadCount);
        Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "개인.dat")));
        Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "기관.dat")));
        Assert.False(File.Exists(Path.Combine(destinationDirectory.Path, "VI발동.dat")));
        Assert.False(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.MarkerFileName)));
        Assert.Empty(Directory.EnumerateFiles(destinationDirectory.Path, "*.tmp"));
    }

    [Fact]
    public async Task Unsafe_rollback_is_reported_as_outcome_unknown_and_is_never_retried()
    {
        using var destinationDirectory = new TemporaryDirectory();
        PreparePrivate(destinationDirectory.Path);
        var source = new CountingSource(Snapshot());
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            source,
            utcNow: null,
            afterDataFileMoved: movedCount =>
            {
                if (movedCount != 1)
                {
                    return;
                }

                File.WriteAllText(Path.Combine(destinationDirectory.Path, "개인.dat"), "tampered");
                Directory.CreateDirectory(Path.Combine(destinationDirectory.Path, "외국인.dat"));
            });

        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.RollbackFailed, error.Failure);
        Assert.True(error.OutcomeUnknown);
        Assert.Equal(1, source.ReadCount);
        Assert.True(File.Exists(Path.Combine(destinationDirectory.Path, "개인.dat")));
        Assert.False(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.MarkerFileName)));
        Assert.True(File.Exists(Path.Combine(
            destinationDirectory.Path,
            LegacyManualOperatorDataImporter.IntentFileName)));
        Assert.Equal(
            LegacyManualOperatorDataRuntimeState.InterruptedImport,
            LegacyManualOperatorDataRuntimeGuard.Inspect(destinationDirectory.Path).State);
    }

    [Fact]
    public async Task Invalid_source_format_creates_no_private_file_and_does_not_disclose_the_path()
    {
        using var sourceDirectory = new TemporaryDirectory();
        using var destinationDirectory = new TemporaryDirectory();
        WriteSourceFiles(sourceDirectory.Path);
        await File.WriteAllTextAsync(Path.Combine(sourceDirectory.Path, "기관.dat"), "invalid");
        PreparePrivate(destinationDirectory.Path);
        using var importer = new LegacyManualOperatorDataImporter(
            destinationDirectory.Path,
            new FixedLegacyManualOperatorDataSource(sourceDirectory.Path, TimeSpan.Zero));

        var error = await Assert.ThrowsAsync<LegacyManualOperatorImportException>(
            () => importer.ImportAsync());

        Assert.Equal(LegacyManualOperatorImportFailure.SourceInvalid, error.Failure);
        Assert.DoesNotContain(sourceDirectory.Path, error.Message, StringComparison.OrdinalIgnoreCase);
        Assert.Empty(Directory.EnumerateFileSystemEntries(destinationDirectory.Path));
    }

    private static void WriteSourceFiles(string directory)
    {
        foreach (var fileName in DestinationFiles.Take(3))
        {
            File.WriteAllBytes(
                Path.Combine(directory, fileName),
                LegacyManualOperatorDataCodec.SerializeNetSell(Rows()));
        }

        File.WriteAllBytes(
            Path.Combine(directory, "VI발동.dat"),
            LegacyManualOperatorDataCodec.SerializeVi(ViItems()));
    }

    private static LegacyManualOperatorSourceSnapshot Snapshot() => new(
        Rows(),
        Rows(),
        Rows(),
        ViItems(),
        new string('a', 64));

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

    private static IReadOnlyList<LegacyManualViItem> ViItems() =>
    [
        new("PKR7005930003", "삼성전자"),
        new("PKR7016360000", "삼성증권"),
        new("PKR7005930003", "삼성전자")
    ];

    private static void PreparePrivate(string path) =>
        TrustedManualDirectory.PreparePrivateWritableDirectory(path);

    private sealed class CountingSource : ILegacyManualOperatorDataSource
    {
        private readonly LegacyManualOperatorSourceSnapshot _snapshot;

        public CountingSource(LegacyManualOperatorSourceSnapshot snapshot)
        {
            _snapshot = snapshot;
        }

        public int ReadCount { get; private set; }

        public Task<LegacyManualOperatorSourceSnapshot> ReadAsync(
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            ReadCount++;
            return Task.FromResult(_snapshot);
        }
    }

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