using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
using RealCV.Application.Interfaces;
using RealCV.Application.Models;
using RealCV.Domain.Entities;
using RealCV.Infrastructure.Configuration;
using RealCV.Infrastructure.Data;
using RealCV.Infrastructure.ExternalApis;
using RealCV.Infrastructure.Services;

namespace RealCV.Tests.Services;

public class CompanyVerifierServiceTests : IDisposable
{
    private readonly Mock<HttpMessageHandler> _mockHttpHandler;
    private readonly Mock<ILogger<CompanyVerifierService>> _mockServiceLogger;
    private readonly Mock<ILogger<CompaniesHouseClient>> _mockClientLogger;
    private readonly Mock<ICompanyNameMatcherService> _mockAiMatcher;
    private readonly ApplicationDbContext _dbContext;
    private readonly CompanyVerifierService _sut;
    private readonly HttpClient _httpClient;
    private readonly DbContextOptions<ApplicationDbContext> _dbOptions;
    private readonly InMemoryDatabaseRoot _dbRoot;

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
        PropertyNameCaseInsensitive = true
    };

    public CompanyVerifierServiceTests()
    {
        _mockHttpHandler = new Mock<HttpMessageHandler>();
        _mockServiceLogger = new Mock<ILogger<CompanyVerifierService>>();
        _mockClientLogger = new Mock<ILogger<CompaniesHouseClient>>();
        _mockAiMatcher = new Mock<ICompanyNameMatcherService>();

        _httpClient = new HttpClient(_mockHttpHandler.Object);

        var settings = Options.Create(new CompaniesHouseSettings
        {
            ApiKey = "test-api-key",
            BaseUrl = "https://api.company-information.service.gov.uk"
        });

        var client = new CompaniesHouseClient(_httpClient, settings, _mockClientLogger.Object);

        // Use a shared InMemoryDatabaseRoot so all contexts share the same data store
        _dbRoot = new InMemoryDatabaseRoot();
        var dbName = Guid.NewGuid().ToString();

        _dbOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: dbName, databaseRoot: _dbRoot)
            .ConfigureWarnings(w => w.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning))
            .Options;

        _dbContext = new ApplicationDbContext(_dbOptions);

        // Create a mock factory that returns a new context with the same in-memory database
        var mockFactory = new Mock<IDbContextFactory<ApplicationDbContext>>();
        mockFactory.Setup(f => f.CreateDbContext())
            .Returns(() => new ApplicationDbContext(_dbOptions));
        mockFactory.Setup(f => f.CreateDbContextAsync(It.IsAny<CancellationToken>()))
            .ReturnsAsync(() => new ApplicationDbContext(_dbOptions));

        // Setup AI matcher to return matching results for exact company name matches
        _mockAiMatcher.Setup(m => m.FindBestMatchAsync(
                It.IsAny<string>(),
                It.IsAny<List<CompanyCandidate>>(),
                It.IsAny<string?>(),
                It.IsAny<CancellationToken>()))
            .Returns((string cvCompanyName, List<CompanyCandidate> candidates, string? industryHint, CancellationToken _) =>
            {
                // Find exact or close match in candidates
                var exactMatch = candidates.FirstOrDefault(c =>
                    c.CompanyName.Equals(cvCompanyName, StringComparison.OrdinalIgnoreCase));

                if (exactMatch != null)
                {
                    return Task.FromResult<SemanticMatchResult?>(new SemanticMatchResult
                    {
                        CandidateCompanyName = exactMatch.CompanyName,
                        CandidateCompanyNumber = exactMatch.CompanyNumber,
                        ConfidenceScore = 100,
                        MatchType = "Exact",
                        Reasoning = "Exact name match"
                    });
                }

                // Try fuzzy match for close names (e.g., with/without Ltd)
                var fuzzyMatch = candidates.FirstOrDefault(c =>
                    c.CompanyName.Contains(cvCompanyName, StringComparison.OrdinalIgnoreCase) ||
                    cvCompanyName.Contains(c.CompanyName.Replace(" Ltd", "").Replace(" Limited", ""), StringComparison.OrdinalIgnoreCase));

                if (fuzzyMatch != null)
                {
                    return Task.FromResult<SemanticMatchResult?>(new SemanticMatchResult
                    {
                        CandidateCompanyName = fuzzyMatch.CompanyName,
                        CandidateCompanyNumber = fuzzyMatch.CompanyNumber,
                        ConfidenceScore = 85,
                        MatchType = "TradingName",
                        Reasoning = "Similar name match"
                    });
                }

                return Task.FromResult<SemanticMatchResult?>(new SemanticMatchResult
                {
                    CandidateCompanyName = "No match",
                    CandidateCompanyNumber = "NONE",
                    ConfidenceScore = 0,
                    MatchType = "NoMatch",
                    Reasoning = "No matching company found"
                });
            });

        _sut = new CompanyVerifierService(client, mockFactory.Object, _mockAiMatcher.Object, _mockServiceLogger.Object);
    }

    public void Dispose()
    {
        _httpClient.Dispose();
        _dbContext.Dispose();
        GC.SuppressFinalize(this);
    }

    #region VerifyCompanyAsync Tests

    [Fact]
    public async Task VerifyCompanyAsync_ExactMatch_ReturnsVerifiedWith100PercentScore()
    {
        // Arrange
        const string companyName = "ACME Corporation Ltd";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", "ACME Corporation Ltd", "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchScore.Should().Be(100);
        result.MatchedCompanyName.Should().Be("ACME Corporation Ltd");
        result.MatchedCompanyNumber.Should().Be("12345678");
        result.ClaimedCompany.Should().Be(companyName);
    }

    [Fact]
    public async Task VerifyCompanyAsync_FuzzyMatchAboveThreshold_ReturnsVerified()
    {
        // Arrange
        // Using names that will produce a match score above 70% threshold
        const string claimedName = "ACME Corporation";
        const string actualName = "ACME Corporation Ltd";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", actualName, "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(claimedName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchScore.Should().BeGreaterThanOrEqualTo(70);
        result.MatchedCompanyName.Should().Be(actualName);
    }

    [Fact]
    public async Task VerifyCompanyAsync_FuzzyMatchBelowThreshold_ReturnsNotVerified()
    {
        // Arrange
        const string claimedName = "Totally Different Company";
        const string actualName = "ACME Corporation Ltd";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", actualName, "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(claimedName, null, null);

        // Assert
        result.IsVerified.Should().BeFalse();
        result.MatchScore.Should().Be(0);
        result.MatchedCompanyName.Should().BeNull();
        result.VerificationNotes.Should().Contain("could not be verified");
    }

    [Fact]
    public async Task VerifyCompanyAsync_CachedCompany_ReturnsCachedWithoutApiCall()
    {
        // Arrange
        const string companyName = "Cached Company Ltd";
        var cachedCompany = new CompanyCache
        {
            CompanyNumber = "99999999",
            CompanyName = "Cached Company Ltd",
            Status = "active",
            CachedAt = DateTime.UtcNow.AddDays(-5) // Within 30-day window
        };

        _dbContext.CompanyCache.Add(cachedCompany);
        await _dbContext.SaveChangesAsync();

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchedCompanyNumber.Should().Be("99999999");
        result.VerificationNotes.Should().BeNull(); // Cached results have no specific notes

        // Verify API was NOT called (no HTTP setup means it would fail if called)
        _mockHttpHandler.Protected().Verify(
            "SendAsync",
            Times.Never(),
            ItExpr.IsAny<HttpRequestMessage>(),
            ItExpr.IsAny<CancellationToken>());
    }

    [Fact]
    public async Task VerifyCompanyAsync_ExpiredCache_TriggersNewApiCall()
    {
        // Arrange
        const string companyName = "Expired Company Ltd";
        var expiredCachedCompany = new CompanyCache
        {
            CompanyNumber = "88888888",
            CompanyName = "Expired Company Ltd",
            Status = "active",
            CachedAt = DateTime.UtcNow.AddDays(-35) // Beyond 30-day window
        };

        _dbContext.CompanyCache.Add(expiredCachedCompany);
        await _dbContext.SaveChangesAsync();

        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", "Expired Company Ltd", "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchedCompanyNumber.Should().Be("12345678"); // From API, not cache

        // Verify API WAS called (at least once - multiple queries are generated for matching)
        _mockHttpHandler.Protected().Verify(
            "SendAsync",
            Times.AtLeastOnce(),
            ItExpr.Is<HttpRequestMessage>(r => r.RequestUri!.ToString().Contains("search/companies")),
            ItExpr.IsAny<CancellationToken>());
    }

    [Fact]
    public async Task VerifyCompanyAsync_NoMatchingCompany_ReturnsNotVerified()
    {
        // Arrange
        const string companyName = "NonExistent Company Ltd";
        var searchResponse = CreateSearchResponse(Array.Empty<CompaniesHouseSearchItemDto>());

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeFalse();
        result.MatchScore.Should().Be(0);
        result.MatchedCompanyName.Should().BeNull();
        result.VerificationNotes.Should().Contain("could not be verified");
    }

    [Fact]
    public async Task VerifyCompanyAsync_ApiRateLimitError_ReturnsNotVerifiedWithAppropriateMessage()
    {
        // Arrange
        const string companyName = "Rate Limited Company";

        SetupHttpResponse<object>(HttpStatusCode.TooManyRequests, null);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeFalse();
        result.MatchScore.Should().Be(0);
        result.VerificationNotes.Should().Contain("rate limiting");
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task VerifyCompanyAsync_EmptyOrNullCompanyName_ThrowsArgumentException(string? companyName)
    {
        // Act
        var act = () => _sut.VerifyCompanyAsync(companyName!, null, null);

        // Assert
        await act.Should().ThrowAsync<ArgumentException>();
    }

    [Fact]
    public async Task VerifyCompanyAsync_WithDates_IncludesDatesInResult()
    {
        // Arrange
        const string companyName = "Dated Company Ltd";
        var startDate = new DateOnly(2020, 1, 15);
        var endDate = new DateOnly(2023, 6, 30);

        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", "Dated Company Ltd", "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, startDate, endDate);

        // Assert
        result.ClaimedStartDate.Should().Be(startDate);
        result.ClaimedEndDate.Should().Be(endDate);
    }

    [Fact]
    public async Task VerifyCompanyAsync_MultipleMatches_ReturnsBestMatch()
    {
        // Arrange
        const string companyName = "Tech Solutions Ltd";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("11111111", "Tech Solutions International", "active"),
            CreateSearchItem("22222222", "Tech Solutions Ltd", "active"),
            CreateSearchItem("33333333", "Old Tech Solutions", "dissolved")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchScore.Should().Be(100);
        result.MatchedCompanyNumber.Should().Be("22222222");
    }

    [Fact]
    public async Task VerifyCompanyAsync_CachesNewlyVerifiedCompany()
    {
        // Arrange
        const string companyName = "New Cacheable Company Ltd";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("77777777", "New Cacheable Company Ltd", "active", "2010-05-15")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert - use AsNoTracking to get fresh data from the store
        var cachedEntry = await _dbContext.CompanyCache
            .AsNoTracking()
            .FirstOrDefaultAsync(c => c.CompanyNumber == "77777777");

        cachedEntry.Should().NotBeNull();
        cachedEntry!.CompanyName.Should().Be("New Cacheable Company Ltd");
        cachedEntry.Status.Should().Be("active");
    }

    [Fact]
    public async Task VerifyCompanyAsync_UpdatesExistingCacheEntry()
    {
        // Arrange
        const string companyName = "Updated Company Ltd";
        var existingCache = new CompanyCache
        {
            CompanyNumber = "55555555",
            CompanyName = "Old Company Name",
            Status = "active",
            CachedAt = DateTime.UtcNow.AddDays(-40)
        };

        _dbContext.CompanyCache.Add(existingCache);
        await _dbContext.SaveChangesAsync();

        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("55555555", "Updated Company Ltd", "dissolved")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        await _sut.VerifyCompanyAsync(companyName, null, null);

        // Assert - use AsNoTracking to get fresh data from the store
        var cachedEntry = await _dbContext.CompanyCache
            .AsNoTracking()
            .FirstOrDefaultAsync(c => c.CompanyNumber == "55555555");

        cachedEntry.Should().NotBeNull();
        cachedEntry!.CompanyName.Should().Be("Updated Company Ltd");
        cachedEntry.Status.Should().Be("dissolved");
        cachedEntry.CachedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5));
    }

    [Fact]
    public async Task VerifyCompanyAsync_CaseInsensitiveMatching_ReturnsVerified()
    {
        // Arrange
        const string claimedName = "acme corporation ltd";
        const string actualName = "ACME CORPORATION LTD";

        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", actualName, "active")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var result = await _sut.VerifyCompanyAsync(claimedName, null, null);

        // Assert
        result.IsVerified.Should().BeTrue();
        result.MatchScore.Should().Be(100);
    }

    #endregion

    #region SearchCompaniesAsync Tests

    [Fact]
    public async Task SearchCompaniesAsync_ValidQuery_ReturnsSearchResults()
    {
        // Arrange
        const string query = "Tech";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("11111111", "Tech Solutions Ltd", "active", "2015-03-20", "London, UK"),
            CreateSearchItem("22222222", "Tech Innovations Inc", "active", "2018-07-10", "Manchester, UK")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var results = await _sut.SearchCompaniesAsync(query);

        // Assert
        results.Should().HaveCount(2);
        results[0].CompanyNumber.Should().Be("11111111");
        results[0].CompanyName.Should().Be("Tech Solutions Ltd");
        results[0].CompanyStatus.Should().Be("active");
        results[0].IncorporationDate.Should().Be(new DateOnly(2015, 3, 20));
        results[0].AddressSnippet.Should().Be("London, UK");
    }

    [Fact]
    public async Task SearchCompaniesAsync_NoResults_ReturnsEmptyList()
    {
        // Arrange
        const string query = "NonexistentXYZ123";
        var searchResponse = CreateSearchResponse(Array.Empty<CompaniesHouseSearchItemDto>());

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var results = await _sut.SearchCompaniesAsync(query);

        // Assert
        results.Should().BeEmpty();
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public async Task SearchCompaniesAsync_EmptyOrNullQuery_ThrowsArgumentException(string? query)
    {
        // Act
        var act = () => _sut.SearchCompaniesAsync(query!);

        // Assert
        await act.Should().ThrowAsync<ArgumentException>();
    }

    [Fact]
    public async Task SearchCompaniesAsync_NullCompanyStatus_ReturnsUnknownStatus()
    {
        // Arrange
        const string query = "Unknown Status Corp";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", "Unknown Status Corp", null)
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var results = await _sut.SearchCompaniesAsync(query);

        // Assert
        results.Should().HaveCount(1);
        results[0].CompanyStatus.Should().Be("Unknown");
    }

    [Fact]
    public async Task SearchCompaniesAsync_InvalidDateFormat_ReturnsNullIncorporationDate()
    {
        // Arrange
        const string query = "Invalid Date Corp";
        var searchResponse = CreateSearchResponse(new[]
        {
            CreateSearchItem("12345678", "Invalid Date Corp", "active", "invalid-date")
        });

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        var results = await _sut.SearchCompaniesAsync(query);

        // Assert
        results.Should().HaveCount(1);
        results[0].IncorporationDate.Should().BeNull();
    }

    [Fact]
    public async Task SearchCompaniesAsync_PassesThroughToApiCorrectly()
    {
        // Arrange
        const string query = "TechCompany";
        var searchResponse = CreateSearchResponse(Array.Empty<CompaniesHouseSearchItemDto>());

        SetupHttpResponse(HttpStatusCode.OK, searchResponse);

        // Act
        await _sut.SearchCompaniesAsync(query);

        // Assert
        _mockHttpHandler.Protected().Verify(
            "SendAsync",
            Times.Once(),
            ItExpr.Is<HttpRequestMessage>(r =>
                r.Method == HttpMethod.Get &&
                r.RequestUri!.ToString().Contains("search/companies") &&
                r.RequestUri.ToString().Contains(query)),
            ItExpr.IsAny<CancellationToken>());
    }

    #endregion

    #region Helper Methods

    private void SetupHttpResponse<T>(HttpStatusCode statusCode, T? content)
    {
        // Return a fresh response for each call to avoid stream disposal issues
        // when multiple API calls are made (e.g., multiple search queries)
        // Also handle both search and company detail endpoints
        _mockHttpHandler
            .Protected()
            .Setup<Task<HttpResponseMessage>>(
                "SendAsync",
                ItExpr.IsAny<HttpRequestMessage>(),
                ItExpr.IsAny<CancellationToken>())
            .ReturnsAsync((HttpRequestMessage request, CancellationToken _) =>
            {
                var url = request.RequestUri?.ToString() ?? "";
                var response = new HttpResponseMessage(statusCode);

                // For search requests, return the search response
                if (url.Contains("search/companies") && content != null)
                {
                    response.Content = JsonContent.Create(content, options: JsonOptions);
                }
                // For company detail requests (e.g., /company/12345678), return a valid company response
                else if (url.Contains("/company/") && !url.Contains("search"))
                {
                    // Extract company number from URL
                    var companyNumber = url.Split("/company/").LastOrDefault()?.Split("/").FirstOrDefault()?.Split("?").FirstOrDefault() ?? "12345678";

                    // Return a minimal valid company response
                    var companyResponse = new
                    {
                        company_number = companyNumber,
                        company_name = "Test Company Ltd",
                        company_status = "active",
                        type = "ltd"
                    };
                    response.Content = JsonContent.Create(companyResponse, options: JsonOptions);
                }
                else if (content != null)
                {
                    response.Content = JsonContent.Create(content, options: JsonOptions);
                }

                return response;
            });
    }

    private static CompaniesHouseSearchResponseDto CreateSearchResponse(
        IEnumerable<CompaniesHouseSearchItemDto> items)
    {
        var itemList = items.ToList();
        return new CompaniesHouseSearchResponseDto
        {
            TotalResults = itemList.Count,
            ItemsPerPage = 20,
            StartIndex = 0,
            Items = itemList
        };
    }

    private static CompaniesHouseSearchItemDto CreateSearchItem(
        string companyNumber,
        string title,
        string? status,
        string? dateOfCreation = null,
        string? addressSnippet = null)
    {
        return new CompaniesHouseSearchItemDto
        {
            CompanyNumber = companyNumber,
            Title = title,
            CompanyStatus = status,
            DateOfCreation = dateOfCreation,
            AddressSnippet = addressSnippet
        };
    }

    #endregion

    #region Test DTOs (matching API response format with snake_case)

    /// <summary>
    /// DTO for test serialization to match Companies House API snake_case format
    /// </summary>
    private sealed record CompaniesHouseSearchResponseDto
    {
        public int TotalResults { get; init; }
        public int ItemsPerPage { get; init; }
        public int StartIndex { get; init; }
        public List<CompaniesHouseSearchItemDto> Items { get; init; } = [];
    }

    private sealed record CompaniesHouseSearchItemDto
    {
        public required string CompanyNumber { get; init; }
        public required string Title { get; init; }
        public string? CompanyStatus { get; init; }
        public string? CompanyType { get; init; }
        public string? DateOfCreation { get; init; }
        public string? DateOfCessation { get; init; }
        public string? AddressSnippet { get; init; }
    }

    #endregion
}
