using Xunit;
using Moq;
using System.Reflection;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using QuantEngine.Application.Interfaces;
using QuantEngine.Application.Models;
using QuantEngine.Core.Interfaces;
using QuantEngine.Application.Services;

namespace QuantEngine.Core.Tests;

public class KisDataCollectionOrchestratorTests
{
    private readonly Mock<IKisApiClient> _kisApiClientMock;
    private readonly Mock<ICollectionWriteRepository> _writeRepositoryMock;
    private readonly Mock<ICollectionReadRepository> _readRepositoryMock;
    private readonly Mock<ILogger<KisDataCollectionOrchestrator>> _loggerMock;
    private readonly Mock<IRuntimeAuditTrailService> _auditTrailMock;
    private readonly PriceDataNormalizer _normalizer;
    private readonly SourcePriorityResolver _priorityResolver;
    private readonly KisDataCollectionOrchestrator _orchestrator;

    public KisDataCollectionOrchestratorTests()
    {
        _kisApiClientMock = new Mock<IKisApiClient>();
        _writeRepositoryMock = new Mock<ICollectionWriteRepository>();
        _readRepositoryMock = new Mock<ICollectionReadRepository>();
        _loggerMock = new Mock<ILogger<KisDataCollectionOrchestrator>>();
        _auditTrailMock = new Mock<IRuntimeAuditTrailService>();
        _priorityResolver = new SourcePriorityResolver();
        _normalizer = new PriceDataNormalizer(_priorityResolver);
        _kisApiClientMock
            .Setup(k => k.GetDailyItemChartPriceAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), "D", It.IsAny<string>()))
            .ReturnsAsync(new Dictionary<string, object>());

        _orchestrator = new KisDataCollectionOrchestrator(
            _kisApiClientMock.Object,
            _writeRepositoryMock.Object,
            _readRepositoryMock.Object,
            _normalizer,
            _priorityResolver,
            _loggerMock.Object,
            _auditTrailMock.Object
        );
    }

    [Fact]
    public async Task RunCollectionAsync_WithCachedSnapshot_ShouldNotCallKisApiClient()
    {
        var mockTime = new DateTime(2026, 7, 13, 13, 0, 0, DateTimeKind.Utc); // 2026-07-13 22:00:00 KST (Market closed)
        _orchestrator.UtcNowProvider = () => mockTime;

        var runId = "test-run-001";
        var ticker = "005930";
        var account = "mock";
        var todayPrefix = mockTime.AddHours(9).ToString("yyyy-MM-dd");

        var cachedSnapshot = new CollectionSnapshotRecord(
            RunId: "prev-run",
            DatasetName: "data_feed",
            Ticker: ticker,
            SourceName: "kis_open_api",
            PayloadJson: """{"Ticker":"005930","current_price":50000}""",
            CapturedAt: $"{todayPrefix}T14:30:00"
        );

        _readRepositoryMock
            .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
            .ReturnsAsync(new List<CollectionSnapshotRecord> { cachedSnapshot });

        _writeRepositoryMock
            .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
            .Returns(Task.CompletedTask);

        var result = await _orchestrator.RunCollectionAsync(runId, account, new List<string> { ticker });

        Assert.NotNull(result);
        Assert.Equal("COMPLETED", result.Status);
        Assert.Equal(1, result.SuccessCount);

        _kisApiClientMock.Verify(
            k => k.GetCurrentPriceAsync(It.IsAny<string>(), It.IsAny<string>()),
            Times.Never,
            "IsMarketClosed should return true and cached snapshot should be used, so KIS API should not be called"
        );

        _writeRepositoryMock.Verify(
            r => r.SaveSnapshotAsync(It.Is<CollectionSnapshotRecord>(s =>
                s.SourceName.Contains("(Cached)"))),
            Times.Once,
            "Cached snapshot source name should include '(Cached)' suffix"
        );
    }

    [Fact]
    public async Task RunCollectionAsync_WithoutCachedSnapshot_ShouldCallKisApiClient()
    {
        var runId = "test-run-002";
        var ticker = "005930";
        var account = "mock";
        _readRepositoryMock
            .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
            .ReturnsAsync(new List<CollectionSnapshotRecord>());

        _kisApiClientMock
            .Setup(k => k.GetCurrentPriceAsync(ticker, account))
            .ReturnsAsync(new Dictionary<string, object>
            {
                { "Ticker", ticker },
                { "current_price", 50000 },
                { "open", 49900 }
            });

        _kisApiClientMock
            .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
            .ReturnsAsync(new Dictionary<string, object>());

        _writeRepositoryMock
            .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
            .Returns(Task.CompletedTask);

        var result = await _orchestrator.RunCollectionAsync(runId, account, new List<string> { ticker });

        Assert.NotNull(result);
        Assert.Equal("COMPLETED", result.Status);
        Assert.Equal(1, result.SuccessCount);
        _auditTrailMock.Verify(a => a.Append("collection_audit", runId, It.IsAny<CollectionExecutionAudit>()), Times.Exactly(2));

        _kisApiClientMock.Verify(
            k => k.GetCurrentPriceAsync(ticker, account),
            Times.Once,
            "No cached snapshot exists, so KIS API should be called"
        );
    }

    [Fact]
    public async Task RunCollectionAsync_WithPriorDaySnapshot_ShouldCallKisApiClient()
    {
        var runId = "test-run-003";
        var ticker = "005930";
        var account = "mock";
        var priorDay = DateTime.UtcNow.AddHours(9).AddDays(-1).ToString("yyyy-MM-dd");

        var priorDaySnapshot = new CollectionSnapshotRecord(
            RunId: "prev-run",
            DatasetName: "data_feed",
            Ticker: ticker,
            SourceName: "kis_open_api",
            PayloadJson: """{"Ticker":"005930","current_price":49000}""",
            CapturedAt: $"{priorDay}T14:30:00"
        );

        _readRepositoryMock
            .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
            .ReturnsAsync(new List<CollectionSnapshotRecord> { priorDaySnapshot });

        _kisApiClientMock
            .Setup(k => k.GetCurrentPriceAsync(ticker, account))
            .ReturnsAsync(new Dictionary<string, object>
            {
                { "Ticker", ticker },
                { "current_price", 50100 }
            });

        _kisApiClientMock
            .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
            .ReturnsAsync(new Dictionary<string, object>());

        _writeRepositoryMock
            .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
            .Returns(Task.CompletedTask);

        var result = await _orchestrator.RunCollectionAsync(runId, account, new List<string> { ticker });

        Assert.NotNull(result);
        Assert.Equal("COMPLETED", result.Status);
        Assert.Equal(1, result.SuccessCount);

        _kisApiClientMock.Verify(
            k => k.GetCurrentPriceAsync(ticker, account),
            Times.Once,
            "Snapshot is from prior day, not today, so KIS API should be called"
        );
    }

    [Fact]
    public async Task RunCollectionAsync_ShouldCompleteEvenIfLineageWriteFails()
    {
        var runId = "test-run-004";
        var ticker = "005930";
        var account = "mock";

        _readRepositoryMock
            .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
            .ReturnsAsync(new List<CollectionSnapshotRecord>());

        _kisApiClientMock
            .Setup(k => k.GetCurrentPriceAsync(ticker, account))
            .ReturnsAsync(new Dictionary<string, object>
            {
                { "Ticker", ticker },
                { "current_price", 50000 }
            });

        _kisApiClientMock
            .Setup(k => k.GetDailyItemChartPriceAsync(ticker, It.IsAny<string>(), It.IsAny<string>(), "D", account))
            .ReturnsAsync(new Dictionary<string, object>());

        _writeRepositoryMock
            .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
            .Returns(Task.CompletedTask);

        var result = await _orchestrator.RunCollectionAsync(runId, account, new List<string> { ticker });

        Assert.NotNull(result);
        Assert.Equal("COMPLETED", result.Status);
        Assert.Equal(1, result.SuccessCount);

        _loggerMock.Verify(
            l => l.Log(
                LogLevel.Warning,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((v, t) =>
                    v.ToString()!.Contains("Failed to write lineage event") ||
                    v.ToString()!.Contains("lineage")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception?, string>>()),
            Times.Never,
            "Lineage write should succeed in normal case (no directory permission issues)"
        );
    }

    [Fact]
    public async Task RunCollectionAsync_WithMultipleTickers_ShouldHandleSuccessAndErrors()
    {
        var runId = "test-run-005";
        var account = "mock";
        var tickers = new List<string> { "005930", "000660" };

        _readRepositoryMock
            .Setup(r => r.GetLatestSnapshotsForTickerAsync(It.IsAny<string>(), It.IsAny<int>()))
            .ReturnsAsync(new List<CollectionSnapshotRecord>());

        _kisApiClientMock
            .Setup(k => k.GetCurrentPriceAsync("005930", account))
            .ReturnsAsync(new Dictionary<string, object>
            {
                { "Ticker", "005930" },
                { "current_price", 50000 }
            });

        _kisApiClientMock
            .Setup(k => k.GetCurrentPriceAsync("000660", account))
            .ReturnsAsync(new Dictionary<string, object>
            {
                { "Ticker", "000660" },
                { "current_price", 100000 }
            });

        _kisApiClientMock
            .Setup(k => k.GetDailyItemChartPriceAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), "D", account))
            .ReturnsAsync(new Dictionary<string, object>());

        var callCount = 0;
        _writeRepositoryMock
            .Setup(r => r.SaveSnapshotAsync(It.IsAny<CollectionSnapshotRecord>()))
            .Returns((CollectionSnapshotRecord snapshot) =>
            {
                callCount++;
                if (callCount == 2 && snapshot.Ticker == "000660")
                    throw new Exception("Storage Error");
                return Task.CompletedTask;
            });

        _writeRepositoryMock
            .Setup(r => r.SavePriceHistoryDailyAsync(It.IsAny<PriceHistoryDailyRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveErrorAsync(It.IsAny<CollectionErrorRecord>()))
            .Returns(Task.CompletedTask);

        _writeRepositoryMock
            .Setup(r => r.SaveRunAsync(It.IsAny<CollectionRunRecord>()))
            .Returns(Task.CompletedTask);

        var result = await _orchestrator.RunCollectionAsync(runId, account, tickers);

        Assert.NotNull(result);
        Assert.Equal("COMPLETED_WITH_ERRORS", result.Status);
        Assert.Equal(1, result.SuccessCount);
        Assert.Equal(1, result.ErrorCount);

        _writeRepositoryMock.Verify(
            r => r.SaveErrorAsync(It.Is<CollectionErrorRecord>(e =>
                e.Ticker == "000660" && e.ErrorMessage == "Storage Error")),
            Times.Once
        );
    }

    [Fact]
    public void TryParseOhlcvBar_WithValidBar_ShouldReturnRecord()
    {
        var method = typeof(KisDataCollectionOrchestrator).GetMethod(
            "TryParseOhlcvBar",
            BindingFlags.NonPublic | BindingFlags.Static);

        Assert.NotNull(method);

        using var document = JsonDocument.Parse("""
        {
          "stck_bsop_date": "20260711",
          "stck_oprc": "1000",
          "stck_hgpr": "1100",
          "stck_lwpr": "900",
          "stck_clpr": "1050",
          "acml_vol": "12345"
        }
        """);

        object?[] args =
        {
            document.RootElement,
            "005930",
            null,
        };

        var result = (bool)method!.Invoke(null, args)!;

        Assert.True(result);
        var record = Assert.IsType<PriceHistoryDailyRecord>(args[2]);
        Assert.Equal("005930", record.Ticker);
        Assert.Equal(new DateOnly(2026, 7, 11), record.TradeDate);
        Assert.Equal(1000m, record.Open);
        Assert.Equal(1100m, record.High);
        Assert.Equal(900m, record.Low);
        Assert.Equal(1050m, record.Close);
        Assert.Equal(12345L, record.Volume);
        Assert.Equal("kis_open_api", record.Source);
    }

    [Fact]
    public void TryParseOhlcvBar_WithInvalidBar_ShouldReturnFalse()
    {
        var method = typeof(KisDataCollectionOrchestrator).GetMethod(
            "TryParseOhlcvBar",
            BindingFlags.NonPublic | BindingFlags.Static);

        Assert.NotNull(method);

        using var document = JsonDocument.Parse("""
        {
          "stck_bsop_date": "20260711",
          "stck_oprc": "1000",
          "stck_hgpr": "900",
          "stck_lwpr": "1100",
          "stck_clpr": "1050",
          "acml_vol": "12345"
        }
        """);

        object?[] args =
        {
            document.RootElement,
            "005930",
            null,
        };

        var result = (bool)method!.Invoke(null, args)!;

        Assert.False(result);
        Assert.Null(args[2]);
    }

    private static string FindRepoRoot()
    {
        var current = new DirectoryInfo(AppContext.BaseDirectory);
        while (current != null)
        {
            if (Directory.Exists(Path.Combine(current.FullName, ".git")))
            {
                return current.FullName;
            }
            current = current.Parent;
        }

        throw new InvalidOperationException("Repository root not found.");
    }
}


