using Books.Api.Commands.ApiKeys;
using Books.Api.Domain.ApiKeys;
using Books.Api.EventFlow.Repositories;
using Books.Api.Tests.Helpers;
using Books.Api.Tests.Infrastructure;
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection;

namespace Books.Api.Tests.Domain;

/// <summary>
/// Integration tests for ApiKey domain operations.
/// Tests the ApiKeyAggregate via CommandBus since ApiKeys are not exposed via GraphQL.
/// </summary>
[Trait("Category", "Integration")]
public class ApiKeyIntegrationTests(TestWebApplicationFactory factory)
    : IntegrationTestBase(factory)
{
    [Fact]
    public async Task CreateApiKey_CreatesKeySuccessfully()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        var command = new CreateApiKeyCommand(
            apiKeyId,
            "Test API Key",
            keyHash,
            companyId,
            "test-user");

        // Act
        await CommandBus.PublishAsync(command, CancellationToken.None);

        // Assert
        var apiKeys = await Eventually.GetListAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        }, 1);

        apiKeys.Should().ContainSingle();
        apiKeys.First().Name.Should().Be("Test API Key");
        apiKeys.First().CompanyId.Should().Be(companyId);
        apiKeys.First().IsActive.Should().BeTrue();
    }

    [Fact]
    public async Task CreateApiKey_FailsForDuplicate()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        var command = new CreateApiKeyCommand(
            apiKeyId,
            "First API Key",
            keyHash,
            companyId,
            "test-user");

        await CommandBus.PublishAsync(command, CancellationToken.None);

        // Wait for first key to be created
        await Eventually.GetListAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        }, 1);

        // Act - Try to create with same ID
        var duplicateCommand = new CreateApiKeyCommand(
            apiKeyId, // Same ID
            "Duplicate API Key",
            keyHash,
            companyId,
            "test-user");

        var act = async () => await CommandBus.PublishAsync(duplicateCommand, CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<Exception>()
            .Where(e => e.Message.Contains("APIKEY_EXISTS") ||
                       (e.InnerException != null && e.InnerException.Message.Contains("APIKEY_EXISTS")) ||
                       e.Message.Contains("already exists"));
    }

    [Fact]
    public async Task RevokeApiKey_RevokesSuccessfully()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        await CommandBus.PublishAsync(new CreateApiKeyCommand(
            apiKeyId, "Key To Revoke", keyHash, companyId, "test-user"), CancellationToken.None);

        await Eventually.GetListAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        }, 1);

        // Act
        var revokeCommand = new RevokeApiKeyCommand(apiKeyId, "admin-user");
        await CommandBus.PublishAsync(revokeCommand, CancellationToken.None);

        // Assert
        var revokedKey = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            var keys = await repo.GetByCompanyIdAsync(companyId);
            var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
            return key?.IsActive == false ? key : null;
        });

        revokedKey.Should().NotBeNull();
        revokedKey!.IsActive.Should().BeFalse();
        revokedKey.RevokedBy.Should().Be("admin-user");
    }

    [Fact]
    public async Task RevokeApiKey_FailsForAlreadyRevoked()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"test-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        await CommandBus.PublishAsync(new CreateApiKeyCommand(
            apiKeyId, "Key To Double Revoke", keyHash, companyId, "test-user"), CancellationToken.None);

        await Eventually.GetListAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        }, 1);

        // Revoke first time
        await CommandBus.PublishAsync(new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);

        await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            var keys = await repo.GetByCompanyIdAsync(companyId);
            var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
            return key?.IsActive == false ? key : null;
        });

        // Act - Try to revoke again
        var act = async () => await CommandBus.PublishAsync(
            new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);

        // Assert
        await act.Should().ThrowAsync<Exception>()
            .Where(e => e.Message.Contains("APIKEY_REVOKED") ||
                       (e.InnerException != null && e.InnerException.Message.Contains("APIKEY_REVOKED")) ||
                       e.Message.Contains("already revoked"));
    }

    [Fact]
    public async Task GetByCompanyId_ReturnsKeysForCompany()
    {
        // Arrange
        var companyId = $"company-{Guid.NewGuid():N}";
        var otherCompanyId = $"company-{Guid.NewGuid():N}";

        // Create keys for our company
        for (var i = 0; i < 3; i++)
        {
            var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
                System.Text.Encoding.UTF8.GetBytes($"key-{i}-{Guid.NewGuid()}")));
            await CommandBus.PublishAsync(new CreateApiKeyCommand(
                ApiKeyId.New, $"Key {i}", keyHash, companyId, "test-user"), CancellationToken.None);
        }

        // Create key for other company
        var otherKeyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"other-key-{Guid.NewGuid()}")));
        await CommandBus.PublishAsync(new CreateApiKeyCommand(
            ApiKeyId.New, "Other Key", otherKeyHash, otherCompanyId, "test-user"), CancellationToken.None);

        // Act
        var keys = await Eventually.GetListAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByCompanyIdAsync(companyId);
        }, 3);

        // Assert
        keys.Should().HaveCount(3);
        keys.Should().AllSatisfy(k => k.CompanyId.Should().Be(companyId));
    }

    [Fact]
    public async Task GetByIdForValidation_ReturnsActiveKey()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"validation-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        await CommandBus.PublishAsync(new CreateApiKeyCommand(
            apiKeyId, "Validation Key", keyHash, companyId, "test-user"), CancellationToken.None);

        // Act
        var validationDto = await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByIdForValidationAsync(apiKeyId.Value);
        });

        // Assert
        validationDto.Should().NotBeNull();
        validationDto!.ApiKeyId.Should().Be(apiKeyId.Value);
        validationDto.Name.Should().Be("Validation Key");
        validationDto.KeyHash.Should().Be(keyHash);
        validationDto.CompanyId.Should().Be(companyId);
        validationDto.IsActive.Should().BeTrue();
    }

    [Fact]
    public async Task GetByIdForValidation_ReturnsNull_ForRevokedKey()
    {
        // Arrange
        var apiKeyId = ApiKeyId.New;
        var keyHash = Convert.ToBase64String(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes($"revoked-validation-key-{Guid.NewGuid()}")));
        var companyId = $"company-{Guid.NewGuid():N}";

        await CommandBus.PublishAsync(new CreateApiKeyCommand(
            apiKeyId, "Revoked Validation Key", keyHash, companyId, "test-user"), CancellationToken.None);

        await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            return await repo.GetByIdForValidationAsync(apiKeyId.Value);
        });

        // Revoke the key
        await CommandBus.PublishAsync(new RevokeApiKeyCommand(apiKeyId, "admin-user"), CancellationToken.None);

        // Wait for revocation
        await Eventually.GetAsync(async () =>
        {
            var repo = GetService<IApiKeyRepository>();
            var keys = await repo.GetByCompanyIdAsync(companyId);
            var key = keys.FirstOrDefault(k => k.Id == apiKeyId.Value);
            return key?.IsActive == false ? key : null;
        });

        // Act
        var repo = GetService<IApiKeyRepository>();
        var validationDto = await repo.GetByIdForValidationAsync(apiKeyId.Value);

        // Assert - Revoked keys should not be returned for validation
        validationDto.Should().BeNull();
    }

    [Fact]
    public async Task GetByIdForValidation_ReturnsNull_ForNonExistentKey()
    {
        // Arrange
        var nonExistentId = ApiKeyId.New;

        // Act
        var repo = GetService<IApiKeyRepository>();
        var validationDto = await repo.GetByIdForValidationAsync(nonExistentId.Value);

        // Assert
        validationDto.Should().BeNull();
    }
}
