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;

/// <summary>
/// Tests to analyze robustness of perceptual hashing under various camera scanning conditions.
/// </summary>
public class RobustnessAnalysisTests : IDisposable
{
    private readonly ITestOutputHelper _output;
    private readonly string _dbPath;
    private readonly CardDatabase _database;
    private readonly CardRecognitionService _recognitionService;

    public RobustnessAnalysisTests(ITestOutputHelper output)
    {
        _output = output;
        _dbPath = Path.Combine(Path.GetTempPath(), $"scry_robustness_test_{Guid.NewGuid()}.db");
        _database = new CardDatabase(_dbPath);
        _recognitionService = new CardRecognitionService(_database);
    }

    /// <summary>
    /// Test how rotation affects hash matching.
    /// pHash uses DCT which is NOT rotation invariant - this tests the impact.
    /// </summary>
    [Theory]
    [InlineData(5)]   // Slight tilt
    [InlineData(15)]  // Moderate tilt 
    [InlineData(45)]  // Significant rotation
    [InlineData(90)]  // Portrait vs landscape
    [InlineData(180)] // Upside down
    public async Task Rotation_ImpactOnHashDistance(int rotationDegrees)
    {
        var imagePath = FindTestImage("reference_alpha/serra_angel.jpg");
        if (imagePath == null)
        {
            _output.WriteLine("Test image not found, skipping");
            return;
        }

        using var original = SKBitmap.Decode(imagePath);
        Assert.NotNull(original);

        // Compute hash of original
        var originalHash = _recognitionService.ComputeHash(original);

        // Register original in database
        await _database.InsertOracleAsync(new Oracle { Id = "oracle-serra", Name = "Serra Angel" });
        await _database.InsertSetAsync(new Set { Id = "set-lea", Code = "LEA", Name = "Alpha" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "serra-angel",
            OracleId = "oracle-serra",
            SetId = "set-lea",
            SetCode = "LEA",
            Name = "Serra Angel",
            Hash = originalHash
        });
        await _recognitionService.InvalidateCacheAsync();

        // Rotate the image
        using var rotated = RotateImage(original, rotationDegrees);
        var rotatedHash = _recognitionService.ComputeHash(rotated);

        var distance = PerceptualHash.HammingDistance(originalHash, rotatedHash);
        var confidence = PerceptualHash.CalculateConfidence(distance, 192);

        // Try recognition
        var result = await _recognitionService.RecognizeAsync(rotated);

        _output.WriteLine($"Rotation: {rotationDegrees}°");
        _output.WriteLine($"  Hamming distance: {distance}/192 bits");
        _output.WriteLine($"  Confidence: {confidence:P0}");
        _output.WriteLine($"  Recognition: {(result.Success ? "SUCCESS" : "FAILED")}");
        _output.WriteLine($"  Result: {result.Card?.Name ?? result.ErrorMessage}");

        // Document expected behavior
        if (rotationDegrees <= 5)
        {
            // Small rotations might still work
            _output.WriteLine($"  [Small rotation - may still match]");
        }
        else
        {
            // pHash is NOT rotation invariant - this is expected to fail
            _output.WriteLine($"  [pHash is NOT rotation invariant - failure expected]");
        }
    }

    /// <summary>
    /// Test how scaling/distance affects hash matching.
    /// </summary>
    [Theory]
    [InlineData(0.25f)] // Very small in frame (far away)
    [InlineData(0.50f)] // Half size
    [InlineData(0.75f)] // 3/4 size
    [InlineData(1.25f)] // Slightly larger
    [InlineData(2.0f)]  // Double size (close up)
    public async Task Scale_ImpactOnHashDistance(float scaleFactor)
    {
        var imagePath = FindTestImage("reference_alpha/serra_angel.jpg");
        if (imagePath == null)
        {
            _output.WriteLine("Test image not found, skipping");
            return;
        }

        using var original = SKBitmap.Decode(imagePath);
        Assert.NotNull(original);

        var originalHash = _recognitionService.ComputeHash(original);

        await _database.InsertOracleAsync(new Oracle { Id = "oracle-serra", Name = "Serra Angel" });
        await _database.InsertSetAsync(new Set { Id = "set-lea", Code = "LEA", Name = "Alpha" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "serra-angel",
            OracleId = "oracle-serra",
            SetId = "set-lea",
            SetCode = "LEA",
            Name = "Serra Angel",
            Hash = originalHash
        });
        await _recognitionService.InvalidateCacheAsync();

        // Scale the image
        var newWidth = (int)(original.Width * scaleFactor);
        var newHeight = (int)(original.Height * scaleFactor);
        using var scaled = ImagePreprocessor.Resize(original, newWidth, newHeight);
        var scaledHash = _recognitionService.ComputeHash(scaled);

        var distance = PerceptualHash.HammingDistance(originalHash, scaledHash);
        var confidence = PerceptualHash.CalculateConfidence(distance, 192);

        var result = await _recognitionService.RecognizeAsync(scaled);

        _output.WriteLine($"Scale: {scaleFactor:P0}");
        _output.WriteLine($"  Original: {original.Width}x{original.Height}");
        _output.WriteLine($"  Scaled: {newWidth}x{newHeight}");
        _output.WriteLine($"  Hamming distance: {distance}/192 bits");
        _output.WriteLine($"  Confidence: {confidence:P0}");
        _output.WriteLine($"  Recognition: {(result.Success ? "SUCCESS" : "FAILED")}");

        // pHash should be relatively scale-invariant since it resizes to 32x32 internally
        _output.WriteLine($"  [pHash resizes internally - should be scale invariant]");
    }

    /// <summary>
    /// Test impact of card being placed on different backgrounds.
    /// </summary>
    [Theory]
    [InlineData(0, 0, 0)]       // Black background
    [InlineData(255, 255, 255)] // White background
    [InlineData(128, 128, 128)] // Gray background
    [InlineData(139, 69, 19)]   // Brown (wood table)
    [InlineData(0, 128, 0)]     // Green (playmat)
    public async Task Background_ImpactOnHashDistance(byte r, byte g, byte b)
    {
        var imagePath = FindTestImage("reference_alpha/serra_angel.jpg");
        if (imagePath == null)
        {
            _output.WriteLine("Test image not found, skipping");
            return;
        }

        using var original = SKBitmap.Decode(imagePath);
        Assert.NotNull(original);

        var originalHash = _recognitionService.ComputeHash(original);

        await _database.InsertOracleAsync(new Oracle { Id = "oracle-serra", Name = "Serra Angel" });
        await _database.InsertSetAsync(new Set { Id = "set-lea", Code = "LEA", Name = "Alpha" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "serra-angel",
            OracleId = "oracle-serra",
            SetId = "set-lea",
            SetCode = "LEA",
            Name = "Serra Angel",
            Hash = originalHash
        });
        await _recognitionService.InvalidateCacheAsync();

        // Create image with card on colored background (with padding)
        using var withBackground = PlaceOnBackground(original, new SKColor(r, g, b), 100);
        var bgHash = _recognitionService.ComputeHash(withBackground);

        var distance = PerceptualHash.HammingDistance(originalHash, bgHash);
        var confidence = PerceptualHash.CalculateConfidence(distance, 192);

        var result = await _recognitionService.RecognizeAsync(withBackground);

        _output.WriteLine($"Background: RGB({r},{g},{b})");
        _output.WriteLine($"  Image size: {withBackground.Width}x{withBackground.Height}");
        _output.WriteLine($"  Hamming distance: {distance}/192 bits");
        _output.WriteLine($"  Confidence: {confidence:P0}");
        _output.WriteLine($"  Recognition: {(result.Success ? "SUCCESS" : "FAILED")}");

        // Background WILL affect hash significantly since no card detection/cropping is done
        _output.WriteLine($"  [No card detection - background included in hash - CRITICAL ISSUE]");
    }

    /// <summary>
    /// Test brightness variations (simulating different lighting).
    /// </summary>
    [Theory]
    [InlineData(-50)]  // Darker
    [InlineData(-25)]  // Slightly darker
    [InlineData(25)]   // Slightly brighter
    [InlineData(50)]   // Brighter
    [InlineData(100)]  // Very bright (overexposed)
    public async Task Brightness_ImpactOnHashDistance(int adjustment)
    {
        var imagePath = FindTestImage("reference_alpha/serra_angel.jpg");
        if (imagePath == null)
        {
            _output.WriteLine("Test image not found, skipping");
            return;
        }

        using var original = SKBitmap.Decode(imagePath);
        Assert.NotNull(original);

        var originalHash = _recognitionService.ComputeHash(original);

        await _database.InsertOracleAsync(new Oracle { Id = "oracle-serra", Name = "Serra Angel" });
        await _database.InsertSetAsync(new Set { Id = "set-lea", Code = "LEA", Name = "Alpha" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "serra-angel",
            OracleId = "oracle-serra",
            SetId = "set-lea",
            SetCode = "LEA",
            Name = "Serra Angel",
            Hash = originalHash
        });
        await _recognitionService.InvalidateCacheAsync();

        using var adjusted = AdjustBrightness(original, adjustment);
        var adjustedHash = _recognitionService.ComputeHash(adjusted);

        var distance = PerceptualHash.HammingDistance(originalHash, adjustedHash);
        var confidence = PerceptualHash.CalculateConfidence(distance, 192);

        var result = await _recognitionService.RecognizeAsync(adjusted);

        _output.WriteLine($"Brightness adjustment: {adjustment:+#;-#;0}");
        _output.WriteLine($"  Hamming distance: {distance}/192 bits");
        _output.WriteLine($"  Confidence: {confidence:P0}");
        _output.WriteLine($"  Recognition: {(result.Success ? "SUCCESS" : "FAILED")}");

        // CLAHE should help normalize lighting
        _output.WriteLine($"  [CLAHE preprocessing should help normalize lighting]");
    }

    /// <summary>
    /// Test how perspective distortion affects matching.
    /// </summary>
    [Theory]
    [InlineData(5)]   // Slight perspective
    [InlineData(15)]  // Moderate perspective
    [InlineData(30)]  // Significant perspective
    public async Task Perspective_ImpactOnHashDistance(int perspectiveDegrees)
    {
        var imagePath = FindTestImage("reference_alpha/serra_angel.jpg");
        if (imagePath == null)
        {
            _output.WriteLine("Test image not found, skipping");
            return;
        }

        using var original = SKBitmap.Decode(imagePath);
        Assert.NotNull(original);

        var originalHash = _recognitionService.ComputeHash(original);

        await _database.InsertOracleAsync(new Oracle { Id = "oracle-serra", Name = "Serra Angel" });
        await _database.InsertSetAsync(new Set { Id = "set-lea", Code = "LEA", Name = "Alpha" });
        
        await _database.InsertCardAsync(new Card
        {
            Id = "serra-angel",
            OracleId = "oracle-serra",
            SetId = "set-lea",
            SetCode = "LEA",
            Name = "Serra Angel",
            Hash = originalHash
        });
        await _recognitionService.InvalidateCacheAsync();

        // Apply perspective transform (shear as approximation)
        using var perspective = ApplyPerspective(original, perspectiveDegrees);
        var perspectiveHash = _recognitionService.ComputeHash(perspective);

        var distance = PerceptualHash.HammingDistance(originalHash, perspectiveHash);
        var confidence = PerceptualHash.CalculateConfidence(distance, 192);

        var result = await _recognitionService.RecognizeAsync(perspective);

        _output.WriteLine($"Perspective: {perspectiveDegrees}° tilt");
        _output.WriteLine($"  Hamming distance: {distance}/192 bits");
        _output.WriteLine($"  Confidence: {confidence:P0}");
        _output.WriteLine($"  Recognition: {(result.Success ? "SUCCESS" : "FAILED")}");

        _output.WriteLine($"  [No perspective correction - distortion affects hash]");
    }

    /// <summary>
    /// Test real-world photos vs reference images.
    /// </summary>
    [Fact]
    public async Task RealPhotos_VsReferenceImages()
    {
        // Find the production database
        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($"Database not found at {dbPath}");
            return;
        }

        using var prodDb = new CardDatabase(dbPath);
        using var prodRecognition = new CardRecognitionService(prodDb);

        var testImagesDir = Path.Combine(rootDir, "TestImages");
        var categoriesToTest = new[] { "real_photos", "varying_quality", "angled", "low_light" };

        _output.WriteLine("=== Real-World Recognition Test ===");
        _output.WriteLine($"Database cards: {(await prodDb.GetCardsWithHashAsync()).Count}");
        _output.WriteLine("");

        foreach (var category in categoriesToTest)
        {
            var categoryPath = Path.Combine(testImagesDir, category);
            if (!Directory.Exists(categoryPath))
                continue;

            _output.WriteLine($"--- {category} ---");
            
            var imageFiles = Directory.GetFiles(categoryPath)
                .Where(f => new[] { ".jpg", ".jpeg", ".png", ".webp" }
                    .Contains(Path.GetExtension(f).ToLowerInvariant()))
                .Take(5)
                .ToList();

            var successes = 0;
            var failures = 0;

            foreach (var file in imageFiles)
            {
                try
                {
                    using var bitmap = SKBitmap.Decode(file);
                    if (bitmap == null) continue;

                    var result = await prodRecognition.RecognizeAsync(bitmap);
                    var fileName = Path.GetFileName(file);

                    if (result.Success)
                    {
                        successes++;
                        _output.WriteLine($"  [OK] {fileName} -> {result.Card?.Name} ({result.Confidence:P0})");
                    }
                    else
                    {
                        failures++;
                        _output.WriteLine($"  [FAIL] {fileName} -> {result.ErrorMessage}");
                    }
                }
                catch (Exception ex)
                {
                    _output.WriteLine($"  [ERROR] {Path.GetFileName(file)}: {ex.Message}");
                }
            }

            _output.WriteLine($"  Results: {successes} OK, {failures} failed");
            _output.WriteLine("");
        }
    }

    #region Helper Methods

    private static string? FindTestImage(string relativePath)
    {
        var currentDir = Directory.GetCurrentDirectory();
        var rootDir = currentDir;
        while (!Directory.Exists(Path.Combine(rootDir, ".git")) && Directory.GetParent(rootDir) != null)
        {
            rootDir = Directory.GetParent(rootDir)!.FullName;
        }
        
        var fullPath = Path.Combine(rootDir, "TestImages", relativePath);
        return File.Exists(fullPath) ? fullPath : null;
    }

    private static SKBitmap RotateImage(SKBitmap original, int degrees)
    {
        var radians = degrees * Math.PI / 180;
        var cos = Math.Abs(Math.Cos(radians));
        var sin = Math.Abs(Math.Sin(radians));
        
        var newWidth = (int)(original.Width * cos + original.Height * sin);
        var newHeight = (int)(original.Width * sin + original.Height * cos);
        
        var rotated = new SKBitmap(newWidth, newHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
        
        using var canvas = new SKCanvas(rotated);
        canvas.Clear(SKColors.White);
        canvas.Translate(newWidth / 2f, newHeight / 2f);
        canvas.RotateDegrees(degrees);
        canvas.Translate(-original.Width / 2f, -original.Height / 2f);
        canvas.DrawBitmap(original, 0, 0);
        
        return rotated;
    }

    private static SKBitmap PlaceOnBackground(SKBitmap card, SKColor bgColor, int padding)
    {
        var newWidth = card.Width + padding * 2;
        var newHeight = card.Height + padding * 2;
        
        var result = new SKBitmap(newWidth, newHeight, SKColorType.Rgba8888, SKAlphaType.Premul);
        
        using var canvas = new SKCanvas(result);
        canvas.Clear(bgColor);
        canvas.DrawBitmap(card, padding, padding);
        
        return result;
    }

    private static SKBitmap AdjustBrightness(SKBitmap original, int adjustment)
    {
        var result = new SKBitmap(original.Width, original.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
        
        for (var y = 0; y < original.Height; y++)
        {
            for (var x = 0; x < original.Width; x++)
            {
                var pixel = original.GetPixel(x, y);
                var r = (byte)Math.Clamp(pixel.Red + adjustment, 0, 255);
                var g = (byte)Math.Clamp(pixel.Green + adjustment, 0, 255);
                var b = (byte)Math.Clamp(pixel.Blue + adjustment, 0, 255);
                result.SetPixel(x, y, new SKColor(r, g, b, pixel.Alpha));
            }
        }
        
        return result;
    }

    private static SKBitmap ApplyPerspective(SKBitmap original, int degrees)
    {
        // Approximate perspective with horizontal shear
        var shearFactor = (float)Math.Tan(degrees * Math.PI / 180);
        var extraWidth = (int)(original.Height * Math.Abs(shearFactor));
        
        var result = new SKBitmap(original.Width + extraWidth, original.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
        
        using var canvas = new SKCanvas(result);
        canvas.Clear(SKColors.White);
        
        var matrix = SKMatrix.CreateSkew(shearFactor, 0);
        if (shearFactor > 0)
        {
            matrix = SKMatrix.Concat(SKMatrix.CreateTranslation(extraWidth, 0), matrix);
        }
        
        canvas.SetMatrix(matrix);
        canvas.DrawBitmap(original, 0, 0);
        
        return result;
    }

    #endregion

    public void Dispose()
    {
        _recognitionService.Dispose();
        _database.Dispose();
        Microsoft.Data.Sqlite.SqliteConnection.ClearAllPools();
        try
        {
            if (File.Exists(_dbPath))
                File.Delete(_dbPath);
        }
        catch { }
    }
}
