using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Predictalytics.Application.Interfaces;
using Predictalytics.Domain.Entities;
using Predictalytics.Domain.Enums;
using Predictalytics.Domain.Interfaces;
using Predictalytics.Infrastructure.Data;
using Predictalytics.Infrastructure.Data.Repositories;
using Predictalytics.Infrastructure.Services;
using Predictalytics.Worker.Services;
using Moq;
using Xunit;

namespace Predictalytics.Application.Tests.Services;

public class IngestModeTests
{
    [Fact]
    public async Task PollingWorker_BypassesSnapshotOnlyTrader()
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        connection.Open();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;

        using (var setup = new AppDbContext(options))
        {
            setup.Database.EnsureCreated();
            setup.Traders.Add(new Trader
            {
                Id = 1,
                PlatformUserId = "0x1",
                DisplayName = "HN1",
                Platform = PlatformType.Polymarket,
                IngestMode = IngestMode.SnapshotOnly
            });
            setup.SaveChanges();
        }

        var services = new ServiceCollection();
        services.AddScoped(_ => new AppDbContext(options));
        services.AddScoped<ITraderRepository, TraderRepository>();
        
        var mockTradeRepo = new Mock<ITradeRepository>();
        mockTradeRepo.Setup(r => r.GetKnownPlatformTradeIdsAsync(It.IsAny<PlatformType>(), It.IsAny<int>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new HashSet<string>());
        services.AddSingleton(mockTradeRepo.Object);

        services.AddScoped<IMarketRepository, MarketRepository>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));

        var mockProvider = new Mock<IPlatformProvider>();
        mockProvider.Setup(p => p.Platform).Returns(PlatformType.Polymarket);
        mockProvider.Setup(p => p.IsImplemented).Returns(true);
        mockProvider.Setup(p => p.PlatformName).Returns("Polymarket");
        
        mockProvider.Setup(p => p.GetTraderTradesAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Trade> { new Trade { PlatformTradeId = "test_tx" } });

        services.AddSingleton<IEnumerable<IPlatformProvider>>(new[] { mockProvider.Object });
        
        var mockRateLimiter = new Mock<IRateLimiter>();
        services.AddSingleton(mockRateLimiter.Object);

        var mockStats = new Mock<IPlatformStatisticsService>();
        services.AddSingleton(mockStats.Object);

        using var provider = services.BuildServiceProvider();

        var worker = new PollingWorker(provider, NullLogger<PollingWorker>.Instance, mockStats.Object);
        
        var method = typeof(PollingWorker).GetMethod("ExecuteAsync", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        Assert.NotNull(method);

        using var cts = new CancellationTokenSource();
        var task = (Task)method!.Invoke(worker, new object[] { cts.Token })!;

        // Wait for worker to start, process, and then cancel
        await Task.Delay(6000);
        cts.Cancel();
        
        try { await task; } catch (Exception) { }

        // Assert: mockProvider GetTraderTradesAsync should NEVER be called since SnapshotOnly is skipped
        mockProvider.Verify(p => p.GetTraderTradesAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Fact]
    public async Task AggregatedMode_AggregatesTradesCorrectly()
    {
        using var connection = new SqliteConnection("DataSource=:memory:");
        connection.Open();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;

        using (var setup = new AppDbContext(options))
        {
            setup.Database.EnsureCreated();
            setup.Traders.Add(new Trader
            {
                Id = 2,
                PlatformUserId = "0x2",
                DisplayName = "AggregatedTrader",
                Platform = PlatformType.Polymarket,
                IngestMode = IngestMode.Aggregated
            });
            
            var ev = new Event { Id = 1, Platform = PlatformType.Polymarket, Slug = "e", Title = "E" };
            setup.Set<Event>().Add(ev);

            var market = new Market { Id = 10, EventId = 1, PlatformMarketId = 1L, Question = "Q?" };
            market.Outcomes.Add(new MarketOutcome { Id = 100, MarketId = 10, Label = "Yes", TokenId = "t100", CurrentPrice = 0.45m });
            setup.Markets.Add(market);
            
            setup.SaveChanges();
        }

        var services = new ServiceCollection();
        services.AddScoped(_ => new AppDbContext(options));
        services.AddScoped<ITraderRepository, TraderRepository>();
        
        var mockTradeRepo = new Mock<ITradeRepository>();
        mockTradeRepo.Setup(r => r.GetKnownPlatformTradeIdsAsync(It.IsAny<PlatformType>(), It.IsAny<int>(), It.IsAny<IEnumerable<string>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new HashSet<string>());
        
        // Mock AddRangeAsync to bypass MySql raw query and save trades directly to SQLite in-memory DB
        mockTradeRepo.Setup(r => r.AddRangeAsync(It.IsAny<IEnumerable<Trade>>(), It.IsAny<CancellationToken>()))
            .Callback<IEnumerable<Trade>, CancellationToken>((trades, ct) =>
            {
                using var db = new AppDbContext(options);
                foreach (var t in trades)
                {
                    db.Trades.Add(t);
                }
                db.SaveChanges();
            })
            .Returns(Task.CompletedTask);
            
        services.AddSingleton(mockTradeRepo.Object);

        services.AddScoped<IMarketRepository, MarketRepository>();
        services.AddSingleton(typeof(ILogger<>), typeof(NullLogger<>));

        var baseTime = new DateTime(2026, 7, 19, 10, 30, 0, DateTimeKind.Utc);

        var mockProvider = new Mock<IPlatformProvider>();
        mockProvider.Setup(p => p.Platform).Returns(PlatformType.Polymarket);
        mockProvider.Setup(p => p.IsImplemented).Returns(true);
        mockProvider.Setup(p => p.PlatformName).Returns("Polymarket");
        
        // Fetch returns two trades in the same hour
        mockProvider.Setup(p => p.GetTraderTradesAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new List<Trade> 
            { 
                new Trade 
                { 
                    PlatformTradeId = "tx1", 
                    MarketId = "10",
                    AssetId = "t100",
                    Side = TradeSide.Buy, 
                    Price = 0.40m, 
                    Size = 100m, 
                    Amount = 40m, 
                    ExecutedAt = baseTime 
                },
                new Trade 
                { 
                    PlatformTradeId = "tx2", 
                    MarketId = "10",
                    AssetId = "t100",
                    Side = TradeSide.Buy, 
                    Price = 0.60m, 
                    Size = 100m, 
                    Amount = 60m, 
                    ExecutedAt = baseTime.AddMinutes(15) 
                }
            });

        services.AddSingleton<IEnumerable<IPlatformProvider>>(new[] { mockProvider.Object });
        
        var mockRateLimiter = new Mock<IRateLimiter>();
        services.AddSingleton(mockRateLimiter.Object);

        var mockStats = new Mock<IPlatformStatisticsService>();
        services.AddSingleton(mockStats.Object);

        using var provider = services.BuildServiceProvider();

        var worker = new PollingWorker(provider, NullLogger<PollingWorker>.Instance, mockStats.Object);
        
        var method = typeof(PollingWorker).GetMethod("ExecuteAsync", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
        Assert.NotNull(method);

        using var cts = new CancellationTokenSource();
        var task = (Task)method!.Invoke(worker, new object[] { cts.Token })!;

        // Wait for worker to start, process, and then cancel
        await Task.Delay(6000);
        cts.Cancel();
        
        try { await task; } catch (Exception) { }

        // Verify that only 1 aggregated trade is saved in the database
        using (var assertCtx = new AppDbContext(options))
        {
            var trades = await assertCtx.Trades.ToListAsync();
            Assert.Single(trades);
            var aggTrade = trades[0];
            Assert.Equal("AGG_2_100_Buy_2026071910", aggTrade.PlatformTradeId);
            Assert.Equal(200m, aggTrade.Size);
            Assert.Equal(100m, aggTrade.Amount);
            Assert.Equal(0.50m, aggTrade.Price); // VWAP: (40 + 60) / (100 + 100) = 0.50
            Assert.Equal(2, aggTrade.AggregatedCount);
        }
    }
}
