using Books.Api.EventFlow.Repositories;
using Books.Api.EventFlow.Subscribers;
using Books.Api.Tests.Helpers;
using Books.Api.Tests.Infrastructure;
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection;

namespace Books.Api.Tests.GraphQL;

/// <summary>
/// Integration tests for ChartOfAccountsInitializer.
/// Verifies that creating a company automatically bootstraps the standard Danish chart of accounts.
/// </summary>
[Trait("Category", "Integration")]
public class ChartOfAccountsInitializerTests(TestWebApplicationFactory factory)
    : IntegrationTestBase(factory)
{
    [Fact]
    public async Task CreateCompany_AutomaticallyCreatesStandardAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);
        var expectedAccountCount = StandardDanishAccounts.GetAll().Count();

        // Act - Create a company
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) {
                    id
                    name
                }
            }
            """,
            new
            {
                input = new
                {
                    name = "Auto-Account Test Virksomhed"
                }
            });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Wait for accounts to be created (eventually consistent)
        var accounts = await Eventually.GetListAsync(
            async () =>
            {
                var repo = GetService<IAccountRepository>();
                return await repo.GetByCompanyIdAsync(companyId);
            },
            expectedAccountCount,
            timeout: TimeSpan.FromSeconds(30), // Allow more time for many accounts
            failMessage: $"Expected {expectedAccountCount} accounts to be created");

        accounts.Should().HaveCount(expectedAccountCount);
    }

    [Fact]
    public async Task CreateCompany_CreatesRevenueAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Revenue Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check revenue accounts (1xxx)
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var revenue = all.Where(a => a.AccountType == "revenue").ToList();
            return revenue.Count >= 5 ? revenue : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().NotBeEmpty();
        accounts.Should().Contain(a => a.AccountNumber == "1000"); // Salg af varer, DK
        accounts.Should().Contain(a => a.AccountNumber == "1200"); // Salg af ydelser, DK
        accounts.Should().OnlyContain(a => a.AccountType == "revenue");
    }

    [Fact]
    public async Task CreateCompany_CreatesExpenseAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Expense Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var expenses = all.Where(a => a.AccountType == "expense").ToList();
            return expenses.Count >= 10 ? expenses : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().NotBeEmpty();
        accounts.Should().Contain(a => a.AccountNumber == "4000"); // Annoncer og reklame
        accounts.Should().Contain(a => a.AccountNumber == "5000"); // Husleje
        accounts.Should().Contain(a => a.AccountNumber == "7320"); // Køb af software
    }

    [Fact]
    public async Task CreateCompany_CreatesSystemAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "System Accounts Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check system accounts exist (we have many: equity, revenue, debtors, creditors, tax)
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var system = all.Where(a => a.IsSystemAccount).ToList();
            return system.Count >= 15 ? system : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().NotBeEmpty();
        accounts.Should().Contain(a => a.AccountNumber == "3000"); // Aktiekapital
        accounts.Should().Contain(a => a.AccountNumber == "3900"); // Overført resultat
        accounts.Should().Contain(a => a.AccountNumber == "3910"); // Årets resultat
        accounts.Should().Contain(a => a.AccountNumber == "1900"); // Debitorer
        accounts.Should().Contain(a => a.AccountNumber == "6900"); // Kreditorer
        accounts.Should().Contain(a => a.AccountNumber == "7950"); // Skyldig selskabsskat
        accounts.Should().OnlyContain(a => a.IsSystemAccount);
    }

    [Fact]
    public async Task CreateCompany_CreatesLiabilityAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Liability Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var liabilities = all.Where(a => a.AccountType == "liability").ToList();
            return liabilities.Count >= 5 ? liabilities : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().NotBeEmpty();
        accounts.Should().Contain(a => a.AccountNumber == "6900"); // Kreditorer
        accounts.Should().Contain(a => a.AccountNumber == "7900"); // Skyldig moms
        accounts.Should().Contain(a => a.AccountNumber == "7910"); // Skyldig A-skat
    }

    [Fact]
    public async Task CreateCompany_AccountsHaveCorrectVatCodes()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "VAT Code Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check that accounts have correct VAT codes
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            return all.Count >= 50 ? all.ToList() : null;
        }, timeout: TimeSpan.FromSeconds(30));

        // Vareforbrug should have I25 (25% input VAT)
        var vareforbrug = accounts!.FirstOrDefault(a => a.AccountNumber == "2000");
        vareforbrug.Should().NotBeNull();
        vareforbrug!.VatCodeId.Should().Be("I25");

        // Salg af varer DK should have U25 (25% output VAT)
        var salgVarer = accounts.FirstOrDefault(a => a.AccountNumber == "1000");
        salgVarer.Should().NotBeNull();
        salgVarer!.VatCodeId.Should().Be("U25");

        // Bankrenter should have no VAT
        var bankrenter = accounts.FirstOrDefault(a => a.AccountNumber == "9200");
        bankrenter.Should().NotBeNull();
        bankrenter!.VatCodeId.Should().BeNull();
    }

    [Fact]
    public async Task Query_AccountsViaGraphQL_ReturnsBootstrappedAccounts()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);

        // Create company
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "GraphQL Query Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Set company ID header for subsequent requests
        graphqlClient.SetCompanyId(companyId);

        // Wait for accounts to be created
        await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            return all.Count >= 50 ? all : null;
        }, timeout: TimeSpan.FromSeconds(30));

        // Act - Query via GraphQL
        var queryResponse = await graphqlClient.QueryAsync<AccountsResponse>("""
            query Accounts($companyId: ID!) {
                accounts(companyId: $companyId) {
                    id
                    accountNumber
                    name
                    accountType
                    isSystemAccount
                }
            }
            """,
            new { companyId });

        // Assert
        queryResponse.EnsureNoErrors();
        queryResponse.Data!.Accounts.Should().NotBeEmpty();
        queryResponse.Data.Accounts.Should().Contain(a => a.AccountNumber == "2000");
        queryResponse.Data.Accounts.Should().Contain(a => a.Name == "Vareforbrug");
    }

    [Fact]
    public async Task ChartOfAccountsInitializer_IsIdempotent_CalledTwice()
    {
        // Arrange
        var graphqlClient = new GraphQLTestClient(Client);
        var expectedAccountCount = StandardDanishAccounts.GetAll().Count();

        // Create company (which triggers first initialization)
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Idempotency Test Company" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Wait for first initialization to complete
        var initialAccounts = await Eventually.GetListAsync(
            async () =>
            {
                var repo = GetService<IAccountRepository>();
                return await repo.GetByCompanyIdAsync(companyId);
            },
            expectedAccountCount,
            timeout: TimeSpan.FromSeconds(30));

        initialAccounts.Should().HaveCount(expectedAccountCount);

        // Act - Call initializer again (should be idempotent)
        var initializer = GetService<IChartOfAccountsInitializer>();
        await initializer.InitializeAsync(companyId);

        // Assert - Should still have the same number of accounts (not doubled)
        var accountsAfterSecondInit = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        });

        accountsAfterSecondInit.Should().HaveCount(expectedAccountCount,
            "Calling initializer twice should not create duplicate accounts");
    }

    #region Danish Compliance: Chart of Accounts Structure Tests

    [Fact]
    public async Task ChartOfAccounts_ContainsShareCapitalAccount()
    {
        // Arrange - Danish companies are required to have an equity account for share capital
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Share Capital Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check that Aktiekapital (3000) exists
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            return all.Any(a => a.AccountNumber == "3000") ? all.ToList() : null;
        }, timeout: TimeSpan.FromSeconds(30));

        var shareCapital = accounts!.FirstOrDefault(a => a.AccountNumber == "3000");
        shareCapital.Should().NotBeNull("Aktiekapital/Anpartskapital account (3000) is required per Selskabsloven");
        shareCapital!.AccountType.Should().Be("equity");
        shareCapital.IsSystemAccount.Should().BeTrue();
    }

    [Fact]
    public async Task ChartOfAccounts_ContainsCorporateTaxAccount()
    {
        // Arrange - Danish companies need a liability account for corporate tax
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Corporate Tax Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check that Skyldig selskabsskat (7950) exists
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            return all.Any(a => a.AccountNumber == "7950") ? all.ToList() : null;
        }, timeout: TimeSpan.FromSeconds(30));

        var corporateTax = accounts!.FirstOrDefault(a => a.AccountNumber == "7950");
        corporateTax.Should().NotBeNull("Skyldig selskabsskat account (7950) is required per Selskabsloven");
        corporateTax!.AccountType.Should().Be("liability");
        corporateTax.IsSystemAccount.Should().BeTrue();
    }

    [Fact]
    public async Task ChartOfAccounts_HasProperEquityRange()
    {
        // Arrange - Per Danish accounting standards, accounts 3000-3999 should only be Equity
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Equity Range Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check that all 3xxx accounts are Equity type
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var equityRange = all.Where(a =>
                int.TryParse(a.AccountNumber, out var num) && num >= 3000 && num < 4000).ToList();
            return equityRange.Count >= 3 ? equityRange : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().NotBeEmpty("3xxx range should have equity accounts");
        accounts.Should().OnlyContain(
            a => a.AccountType == "equity",
            "Per Danish accounting standards, 3xxx range should ONLY contain equity accounts");
    }

    [Fact]
    public async Task ChartOfAccounts_ContainsFixedAssetAccounts()
    {
        // Arrange - Danish chart of accounts should include fixed asset accounts for depreciation
        var graphqlClient = new GraphQLTestClient(Client);

        // Act
        var createResponse = await graphqlClient.MutateAsync<CreateCompanyResponse>("""
            mutation CreateCompany($input: CreateCompanyInput!) {
                createCompany(input: $input) { id }
            }
            """,
            new { input = new { name = "Fixed Assets Test" } });

        createResponse.EnsureNoErrors();
        var companyId = createResponse.Data!.CreateCompany!.Id;

        // Assert - Check that fixed asset accounts exist (1710-1730)
        var accounts = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IAccountRepository>();
            var all = await repo.GetByCompanyIdAsync(companyId);
            var fixedAssets = all.Where(a => new[] { "1710", "1720", "1730" }.Contains(a.AccountNumber)).ToList();
            return fixedAssets.Count >= 3 ? all.ToList() : null;
        }, timeout: TimeSpan.FromSeconds(30));

        accounts.Should().Contain(a => a.AccountNumber == "1710", "Bygninger account required");
        accounts.Should().Contain(a => a.AccountNumber == "1720", "Maskiner og inventar account required");
        accounts.Should().Contain(a => a.AccountNumber == "1730", "Køretøjer account required");

        // Also verify corresponding depreciation accounts exist
        accounts.Should().Contain(a => a.AccountNumber == "8010", "Afskrivning, bygninger account required");
        accounts.Should().Contain(a => a.AccountNumber == "8020", "Afskrivning, maskiner account required");
        accounts.Should().Contain(a => a.AccountNumber == "8030", "Afskrivning, køretøjer account required");
    }

    #endregion

    // Response DTOs
    private class CreateCompanyResponse { public CompanyDto? CreateCompany { get; set; } }
    private class AccountsResponse { public List<AccountDto> Accounts { get; set; } = []; }
    private class CompanyDto { public string Id { get; set; } = string.Empty; }

    private class AccountDto
    {
        public string Id { get; set; } = string.Empty;
        public string AccountNumber { get; set; } = string.Empty;
        public string Name { get; set; } = string.Empty;
        public string AccountType { get; set; } = string.Empty;
        public string? VatCodeId { get; set; }
        public bool IsSystemAccount { get; set; }
    }
}
