using Microsoft.Data.Sqlite;
using Scry.Core.Data;
using Scry.Core.Imaging;
using Scry.Core.Models;
using Scry.Core.Recognition;
using SkiaSharp;
using Xunit;
using Xunit.Abstractions;

namespace Scry.Tests;

public class CardRecognitionTests : IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly string _dbPath;
    private readonly CardDatabase _database;
    private readonly CardRecognitionService _recognitionService;
    
    public CardRecognitionTests(ITestOutputHelper output)
    {
        _output = output;
        _dbPath = Path.Combine(Path.GetTempPath(), $"scry_recognition_test_{Guid.NewGuid()}.db");
        _database = new CardDatabase(_dbPath);
        _recognitionService = new CardRecognitionService(_database);
    }
    
    [Fact]
    public async Task RecognizeAsync_EmptyDatabase_ReturnsFailed()
    {
        using var bitmap = CreateTestBitmap(100, 100);
        
        var result = await _recognitionService.RecognizeAsync(bitmap);
        
        Assert.False(result.Success);
        Assert.Contains("No cards", result.ErrorMessage);
    }
    
    [Fact]
    public async Task RecognizeAsync_ExactMatch_ReturnsSuccess()
    {
        using var bitmap = CreateTestBitmap(100, 100);
        var hash = _recognitionService.ComputeHash(bitmap);
        
        // Insert oracle and set first
        await _database.InsertOracleAsync(new Oracle { Id = "oracle-test", Name = "Test Card" });
        await _database.InsertSetAsync(new Set { Id = "set-test", Code = "TST", Name = "Test Set" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "test-card",
            OracleId = "oracle-test",
            SetId = "set-test",
            SetCode = "TST",
            Name = "Test Card",
            Hash = hash,
            ImageUri = "https://example.com/test.jpg"
        });
        await _recognitionService.InvalidateCacheAsync();
        
        var result = await _recognitionService.RecognizeAsync(bitmap);
        
        Assert.True(result.Success);
        Assert.Equal("Test Card", result.Card?.Name);
        Assert.Equal(1.0f, result.Confidence);
        Assert.Equal(0, result.HammingDistance);
    }
    
    [Theory]
    [InlineData("reference/brainstorm.png")]
    [InlineData("reference/force_of_will.png")]
    [InlineData("single_cards/llanowar_elves.jpg")]
    public async Task RecognizeAsync_ReferenceImage_SelfMatch(string imagePath)
    {
        var fullPath = Path.Combine("TestImages", imagePath);
        if (!File.Exists(fullPath))
        {
            _output.WriteLine($"Skipping test - file not found: {fullPath}");
            return;
        }
        
        using var bitmap = SKBitmap.Decode(fullPath);
        Assert.NotNull(bitmap);
        
        var hash = _recognitionService.ComputeHash(bitmap);
        var cardName = Path.GetFileNameWithoutExtension(imagePath);
        
        await _database.InsertOracleAsync(new Oracle { Id = $"oracle-{cardName}", Name = cardName });
        await _database.InsertSetAsync(new Set { Id = "set-ref", Code = "REF", Name = "Reference Set" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = cardName,
            OracleId = $"oracle-{cardName}",
            SetId = "set-ref",
            SetCode = "REF",
            Name = cardName,
            Hash = hash
        });
        await _recognitionService.InvalidateCacheAsync();
        
        var result = await _recognitionService.RecognizeAsync(bitmap);
        
        Assert.True(result.Success, $"Recognition failed: {result.ErrorMessage}");
        Assert.Equal(cardName, result.Card?.Name);
        Assert.True(result.Confidence >= 0.85f);
        
        _output.WriteLine($"Matched: {cardName}, Confidence: {result.Confidence:P0}, Distance: {result.HammingDistance}");
    }
    
    [Fact]
    public async Task RecognizeAsync_SerraAngelFromDatabase_Matches()
    {
        // Find the solution root (where .git is)
        var currentDir = Directory.GetCurrentDirectory();
        var rootDir = currentDir;
        while (!Directory.Exists(Path.Combine(rootDir, ".git")) && Directory.GetParent(rootDir) != null)
        {
            rootDir = Directory.GetParent(rootDir)!.FullName;
        }
        
        var dbPath = Path.Combine(rootDir, "src", "Scry.App", "Resources", "Raw", "card_hashes.db");
        if (!File.Exists(dbPath))
        {
            _output.WriteLine($"Skipping - database not found at {dbPath}");
            return;
        }
        
        var imagePath = Path.Combine(rootDir, "TestImages", "reference_alpha", "serra_angel.jpg");
        if (!File.Exists(imagePath))
        {
            _output.WriteLine($"Skipping - image not found at {imagePath}");
            return;
        }
        
        using var testDb = new CardDatabase(dbPath);
        using var testRecognition = new CardRecognitionService(testDb);
        
        using var bitmap = SKBitmap.Decode(imagePath);
        Assert.NotNull(bitmap);
        
        // First, just compute hash and check distance manually
        var queryHash = testRecognition.ComputeHash(bitmap);
        var allCards = await testDb.GetCardsWithHashAsync();
        
        _output.WriteLine($"Query hash length: {queryHash.Length} bytes");
        _output.WriteLine($"Database has {allCards.Count} cards with hashes");
        
        // Find Serra Angel and compute distance
        var serraCard = allCards.FirstOrDefault(c => c.Name == "Serra Angel");
        if (serraCard?.Hash != null)
        {
            var distance = PerceptualHash.HammingDistance(queryHash, serraCard.Hash);
            _output.WriteLine($"Serra Angel hash length: {serraCard.Hash.Length} bytes");
            _output.WriteLine($"Distance to Serra Angel: {distance}");
        }
        
        // Find the actual best match
        int bestDistance = int.MaxValue;
        string? bestName = null;
        foreach (var card in allCards)
        {
            if (card.Hash == null || card.Hash.Length != queryHash.Length) continue;
            var dist = PerceptualHash.HammingDistance(queryHash, card.Hash);
            if (dist < bestDistance)
            {
                bestDistance = dist;
                bestName = card.Name;
            }
        }
        _output.WriteLine($"Best match: {bestName}, distance: {bestDistance}");
        
        // Now try actual recognition
        var result = await testRecognition.RecognizeAsync(bitmap);
        
        if (result.Success)
        {
            _output.WriteLine($"Recognition succeeded: {result.Card?.Name}, confidence: {result.Confidence:P0}");
            Assert.Equal("Serra Angel", result.Card?.Name);
        }
        else
        {
            _output.WriteLine($"Recognition failed: {result.ErrorMessage}");
            // For debugging - this should be 0 since we're using the exact same image
            Assert.Fail($"Expected to match Serra Angel, but got: {result.ErrorMessage}");
        }
    }
    
    [Fact]
    public async Task RecognizeAsync_MeasuresProcessingTime()
    {
        using var bitmap = CreateTestBitmap(200, 300);
        var hash = _recognitionService.ComputeHash(bitmap);
        
        await _database.InsertOracleAsync(new Oracle { Id = "oracle-timing", Name = "Timing Test Card" });
        await _database.InsertSetAsync(new Set { Id = "set-timing", Code = "TST", Name = "Test Set" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "timing-test",
            OracleId = "oracle-timing",
            SetId = "set-timing",
            SetCode = "TST",
            Name = "Timing Test Card",
            Hash = hash
        });
        await _recognitionService.InvalidateCacheAsync();
        
        var result = await _recognitionService.RecognizeAsync(bitmap);
        
        Assert.True(result.Success);
        Assert.True(result.ProcessingTime.TotalMilliseconds > 0);
        _output.WriteLine($"Processing time: {result.ProcessingTime.TotalMilliseconds:F2}ms");
    }
    
    private static SKBitmap CreateTestBitmap(int width, int height)
    {
        var bitmap = new SKBitmap(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
        var random = new Random(42);
        
        for (var y = 0; y < height; y++)
        {
            for (var x = 0; x < width; x++)
            {
                var r = (byte)random.Next(256);
                var g = (byte)random.Next(256);
                var b = (byte)random.Next(256);
                bitmap.SetPixel(x, y, new SKColor(r, g, b));
            }
        }
        
        return bitmap;
    }
    
    public void Dispose()
    {
        _recognitionService.Dispose();
        _database.Dispose();
        SqliteConnection.ClearAllPools();
        try
        {
            if (File.Exists(_dbPath))
            {
                File.Delete(_dbPath);
            }
        }
        catch (IOException)
        {
        }
    }
}
