using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using AwesomeAssertions;
using Books.Api.EventFlow.Repositories;
using Books.Api.Tests.Helpers;
using Books.Api.Tests.Infrastructure;

namespace Books.Api.Tests.Integration;

/// <summary>
/// Integration tests for document processing with AI Bookkeeper.
/// These tests require the AI Bookkeeper service to be running at localhost:8080.
/// Mark with [Trait("Category", "ExternalService")] to allow filtering.
/// </summary>
[Collection(IntegrationTestCollection.Name)]
[Trait("Category", "ExternalService")]
public class DocumentProcessingIntegrationTests : IntegrationTestBase
{
    private readonly GraphQLTestClient _graphqlClient;

    public DocumentProcessingIntegrationTests(TestWebApplicationFactory factory) : base(factory)
    {
        _graphqlClient = new GraphQLTestClient(Client);
    }

    [Fact]
    public async Task ProcessDocument_WithRealInvoice_ReturnsSuggestedJournalLines()
    {
        // Skip if test invoice doesn't exist
        var invoicePath = GetTestInvoicePath();
        if (!File.Exists(invoicePath))
        {
            Console.WriteLine($"SKIP: Test invoice not found at: {invoicePath}");
            return;
        }

        // Arrange - Create company (which auto-creates chart of accounts)
        var companyId = await CreateTestCompanyAsync();

        // Wait for accounts to be created
        await WaitForAccountsAsync(companyId);

        // Create HTTP client for REST API (not GraphQL)
        using var httpClient = Factory.CreateClient();

        // Create multipart form content with invoice
        using var content = new MultipartFormDataContent();
        await using var fileStream = File.OpenRead(invoicePath);
        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
        content.Add(fileContent, "document", "test-invoice.pdf");

        // Act - Call document processing endpoint
        var response = await httpClient.PostAsync(
            $"/api/documents/process?companyId={companyId}",
            content);

        // Assert
        var responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Response status: {response.StatusCode}");
        Console.WriteLine($"Response body: {responseBody}");

        // If AI service is unavailable, show helpful message
        if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
        {
            var error = JsonSerializer.Deserialize<JsonElement>(responseBody);
            var code = error.GetProperty("code").GetString();
            if (code == "AI_UNAVAILABLE")
            {
                Console.WriteLine("\n*** AI Bookkeeper service is not running at localhost:8080 ***");
                Console.WriteLine("Start the AI service to run this test.\n");
                return;
            }
        }

        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = JsonSerializer.Deserialize<JsonElement>(responseBody);

        // Verify extraction data
        result.TryGetProperty("extraction", out var extraction).Should().BeTrue("should have extraction data");
        extraction.TryGetProperty("vendor", out var vendorProp).Should().BeTrue("should extract vendor");
        extraction.TryGetProperty("amount", out var amount).Should().BeTrue("should extract amount");
        amount.GetDecimal().Should().BeGreaterThan(0, "amount should be positive");

        // Verify suggested journal lines (the main thing we're testing!)
        result.TryGetProperty("suggestedLines", out var suggestedLines).Should().BeTrue(
            "should have suggestedLines - this is what we're testing!");

        suggestedLines.GetArrayLength().Should().BeGreaterThanOrEqualTo(2,
            "should have at least 2 lines (expense and creditor)");

        // Verify lines are balanced
        var totalDebits = 0m;
        var totalCredits = 0m;
        Console.WriteLine("\n=== Suggested Journal Lines ===");
        foreach (var line in suggestedLines.EnumerateArray())
        {
            var accountNumber = line.TryGetProperty("accountNumber", out var an) ? an.GetString() : "-";
            var accountName = line.GetProperty("accountName").GetString();
            var debit = line.GetProperty("debitAmount").GetDecimal();
            var credit = line.GetProperty("creditAmount").GetDecimal();
            var vatCode = line.TryGetProperty("vatCode", out var vc) && vc.ValueKind != JsonValueKind.Null
                ? vc.GetString() : null;

            totalDebits += debit;
            totalCredits += credit;

            Console.WriteLine($"  {accountNumber,-6} {accountName,-30} Debit: {debit,10:N2}  Credit: {credit,10:N2}  VAT: {vatCode ?? "-"}");
        }

        totalDebits.Should().Be(totalCredits, "journal entry must be balanced");

        // Verify draft was created
        result.TryGetProperty("draftId", out var draftId).Should().BeTrue();
        draftId.GetString().Should().NotBeNullOrEmpty();

        Console.WriteLine($"\n=== Document Processing Result ===");
        Console.WriteLine($"Draft ID: {draftId.GetString()}");
        Console.WriteLine($"Vendor: {vendorProp.GetString()}");
        Console.WriteLine($"Total Amount: {amount.GetDecimal():N2}");
        Console.WriteLine($"Suggested Lines: {suggestedLines.GetArrayLength()}");
        Console.WriteLine($"Total Debits: {totalDebits:N2}");
        Console.WriteLine($"Total Credits: {totalCredits:N2}");
        Console.WriteLine($"BALANCED: {(totalDebits == totalCredits ? "YES" : "NO")}");
    }

    [Fact]
    public async Task ProcessDocument_WithParkingReceipt_GeneratesSuggestionFromExtraction()
    {
        // This test uses a receipt to verify our fallback suggestion generation
        // works when AI doesn't provide suggestedBooking

        var receiptPath = GetTestInvoicePath("parking_receipt_2582441883.pdf");
        if (!File.Exists(receiptPath))
        {
            Console.WriteLine($"SKIP: Test receipt not found at: {receiptPath}");
            return;
        }

        // Arrange
        var companyId = await CreateTestCompanyAsync();
        await WaitForAccountsAsync(companyId);

        using var httpClient = Factory.CreateClient();
        using var content = new MultipartFormDataContent();
        await using var fileStream = File.OpenRead(receiptPath);
        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
        content.Add(fileContent, "document", "parking-receipt.pdf");

        // Act
        var response = await httpClient.PostAsync(
            $"/api/documents/process?companyId={companyId}",
            content);

        var responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Response: {responseBody}");

        if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
        {
            Console.WriteLine("\n*** AI Bookkeeper service is not running ***\n");
            return;
        }

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var result = JsonSerializer.Deserialize<JsonElement>(responseBody);
        result.TryGetProperty("suggestedLines", out var lines).Should().BeTrue(
            "should generate suggested lines from extraction data");

        // Print the suggestions for manual verification
        Console.WriteLine("\n=== Generated Suggestions (from extraction fallback) ===");
        foreach (var line in lines.EnumerateArray())
        {
            var accountNumber = line.TryGetProperty("accountNumber", out var an) ? an.GetString() : "-";
            var accountName = line.GetProperty("accountName").GetString();
            var debit = line.GetProperty("debitAmount").GetDecimal();
            var credit = line.GetProperty("creditAmount").GetDecimal();
            var vatCode = line.TryGetProperty("vatCode", out var vc) && vc.ValueKind != JsonValueKind.Null
                ? vc.GetString() : null;

            Console.WriteLine($"  {accountNumber,-6} {accountName,-30} Debit: {debit,10:N2}  Credit: {credit,10:N2}  VAT: {vatCode ?? "-"}");
        }
    }

    private async Task<string> CreateTestCompanyAsync()
    {
        var createResponse = await _graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) {
                    id
                    name
                }
            }
            """,
            new
            {
                input = new
                {
                    name = $"AI Test Company {Guid.NewGuid():N}"[..30],
                    cvr = CvrGenerator.Generate()
                }
            });

        createResponse.EnsureNoErrors();
        return createResponse.Data!.CreateCompany!.Id;
    }

    private async Task WaitForAccountsAsync(string companyId)
    {
        // Wait for chart of accounts to be initialized (async event handler)
        await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var accounts = await repo.GetByCompanyIdAsync(companyId);
            // Need at least the key accounts for booking: expense, VAT, creditor
            return accounts.Count >= 50 ? accounts : null;
        }, timeout: TimeSpan.FromSeconds(30));
    }

    private static string GetTestInvoicePath(string fileName = "invoice-F175490.pdf")
    {
        // Try to find test invoice relative to test assembly
        var assemblyDir = Path.GetDirectoryName(typeof(DocumentProcessingIntegrationTests).Assembly.Location)!;

        // Navigate up to find the test-invoices folder
        var searchPaths = new[]
        {
            Path.Combine(assemblyDir, "..", "..", "..", "..", "..", "..", "account-suggestions", "test-invoices", fileName),
            Path.Combine(assemblyDir, "..", "..", "..", "..", "account-suggestions", "test-invoices", fileName),
            $"/Users/nicolajhartmann/projects/books/account-suggestions/test-invoices/{fileName}"
        };

        foreach (var path in searchPaths)
        {
            var fullPath = Path.GetFullPath(path);
            if (File.Exists(fullPath))
            {
                return fullPath;
            }
        }

        return searchPaths[^1]; // Return last (absolute) path for error message
    }

    // Response DTOs
    private class CreateCompanyResponse
    {
        public CompanyDto? CreateCompany { get; set; }
    }

    private class CompanyDto
    {
        public string Id { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
    }
}
