using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.EntityFrameworkCore;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class AiWorkspaceTests
{
    private sealed class FakeAi : ISummarizerService
    {
        public string? Next = "## Result\nGenerated suggestion.";
        public int Calls;
        public string? LastInstruction;
        public string? ActualProvider;
        public string? FallbackReason;
        // The source text the module assembled — what the prompt actually saw.
        public string? LastText;
        public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40)
        {
            Calls++;
            LastInstruction = instruction;
            LastText = text;
            return Task.FromResult(Next);
        }
        public Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30) => Task.FromResult(Next);
        public async Task<AiGenerationResult?> GenerateSectionWithMetadataAsync(
            string instruction,
            string text,
            int maxLength = 180,
            int minLength = 40,
            CancellationToken cancellationToken = default)
        {
            var generated = await SummarizeSectionAsync(instruction, text, maxLength, minLength);
            return generated is null ? null : new AiGenerationResult(generated, ActualProvider, "test-model", FallbackReason,
                FallbackReason is null ? "local_primary" : "external_fallback");
        }
        public Task<AiTextExtractionResult?> ExtractTextAsync(Stream stream, string fileName, string? contentType = null, CancellationToken cancellationToken = default) => throw new NotImplementedException();
        public Task RunProbeAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
        public Task<AiServiceMetrics> GetMetricsAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
    }

    private static (JobTrackerContext db, AiWorkspaceService svc, FakeAi ai) New(string userId)
    {
        var options = new DbContextOptionsBuilder<JobTrackerContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
        var currentUser = new Mock<ICurrentUserService>();
        currentUser.SetupGet(s => s.UserId).Returns(userId);
        var db = new JobTrackerContext(options, currentUser.Object);
        var ai = new FakeAi();
        return (db, new AiWorkspaceService(db, ai), ai);
    }

    private static async Task<int> SeedJobAsync(JobTrackerContext db, string owner)
    {
        var company = new Company { OwnerUserId = owner, Name = "Acme" };
        db.Companies.Add(company);
        await db.SaveChangesAsync();
        var job = new JobApplication { OwnerUserId = owner, CompanyId = company.Id, JobTitle = "Senior Engineer", Status = "Applied", Description = "Build things with C#." };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job.Id;
    }

    private static AiGenerateRequest Req(string module, string? mode = null) => new(module, mode, null);

    [Fact]
    public async Task Generate_stores_an_interaction_and_returns_it()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");

        var res = await svc.GenerateAsync("user-1", jobId, "My CV text", "Ada", Req("job-analysis"), "gemini", default);

        Assert.NotNull(res);
        Assert.Equal("job-analysis", res!.Module);
        Assert.Equal("gemini", res.Provider);
        Assert.Contains("Generated suggestion", res.ResultJson);
        Assert.True(res.InputCharacterCount > 0);
        Assert.Equal("## Result\nGenerated suggestion.".Length, res.OutputCharacterCount);
        Assert.Equal((res.InputCharacterCount + res.OutputCharacterCount + 3) / 4, res.EstimatedTokenCount);
        Assert.Single(await db.AiInteractions.IgnoreQueryFilters().Where(x => x.JobApplicationId == jobId).ToListAsync());
    }

    [Fact]
    public async Task Generate_records_the_actual_provider_and_fallback_metadata()
    {
        var (db, svc, ai) = New("user-1");
        await using var _ = db;
        ai.ActualProvider = "groq";
        ai.FallbackReason = "local_circuit_open";
        var jobId = await SeedJobAsync(db, "user-1");

        var result = await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("job-analysis"), "configured-label", default);

        Assert.Equal("groq", result!.Provider);
        Assert.Contains("local_circuit_open", result.ResultJson);
        Assert.Contains("test-model", result.ResultJson);
    }

    [Fact]
    public async Task Cover_letter_normalizes_an_unknown_mode_and_labels_the_title()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");

        var res = await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("cover-letter", "banana"), "p", default);

        Assert.Equal("professional", res!.Mode);
        Assert.Contains("Professional", res.Title);
    }

    [Fact]
    public async Task Cover_letter_uses_the_linked_cv_current_draft_and_requested_language()
    {
        var (db, svc, ai) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");
        var profile = new StructuredCvProfile
        {
            Contact = new StructuredCvContact { FullName = "Ada Example" },
            Jobs =
            {
                new StructuredCvJob { Id = "visible-role", Title = "Backend Developer", Company = "Initech", Bullets = { "Built truthful .NET services" } },
                new StructuredCvJob { Id = "hidden-role", Title = "Unrelated role", Company = "Hidden Corp", Bullets = { "Unrelated work" } },
            },
            Skills = { "C#", ".NET" },
        };
        db.CareerProfiles.Add(new CareerProfile
        {
            OwnerUserId = "user-1",
            ProfileJson = StructuredCvProfileJson.Serialize(profile),
        });
        db.CvVariants.Add(new CvVariant
        {
            OwnerUserId = "user-1",
            JobApplicationId = jobId,
            Name = "Backend CV",
            PublicSlug = Guid.NewGuid().ToString("N"),
            SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings
            {
                Overrides = { ["hidden-role"] = new CvItemOverride { Hidden = true } },
            }),
            Version = 1,
            CreatedAtUtc = DateTimeOffset.UtcNow,
            UpdatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();

        await svc.GenerateAsync("user-1", jobId, "master profile fallback", "Ada",
            new AiGenerateRequest("cover-letter", "professional", null, "nb-NO", "Existing manual draft", "shorten"), "p", default);

        Assert.Contains("CV BEING USED: Backend CV", ai.LastText);
        Assert.Contains("Built truthful .NET services", ai.LastText);
        Assert.DoesNotContain("Hidden Corp", ai.LastText);
        Assert.Contains("CURRENT DOCUMENT TO REVISE", ai.LastText);
        Assert.Contains("Existing manual draft", ai.LastText);
        Assert.Contains("Norwegian Bokmål", ai.LastInstruction);
        Assert.Contains("Shorten the current cover letter", ai.LastInstruction);
    }

    [Fact]
    public async Task History_is_newest_first_and_filters_by_module()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");
        await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("job-analysis"), "p", default);
        await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("career-match"), "p", default);
        await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("career-match"), "p", default);

        var all = await svc.HistoryAsync("user-1", jobId, null, default);
        Assert.Equal(3, all.Count);
        var match = await svc.HistoryAsync("user-1", jobId, "career-match", default);
        Assert.Equal(2, match.Count);
        Assert.All(match, m => Assert.Equal("career-match", m.Module));
    }

    [Fact]
    public async Task Delete_removes_only_the_owner_row()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");
        var res = await svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("interview"), "p", default);

        Assert.True(await svc.DeleteAsync("user-1", res!.Id, default));
        Assert.Null(await svc.GetAsync("user-1", res.Id, default));
        Assert.False(await svc.DeleteAsync("user-1", res.Id, default));
    }

    [Fact]
    public async Task Unknown_module_is_rejected()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var jobId = await SeedJobAsync(db, "user-1");
        await Assert.ThrowsAsync<ArgumentException>(() => svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("write-my-life-story"), "p", default));
    }

    [Fact]
    public async Task Empty_ai_output_raises_unavailable()
    {
        var (db, svc, ai) = New("user-1");
        await using var _ = db;
        ai.Next = "   ";
        var jobId = await SeedJobAsync(db, "user-1");
        await Assert.ThrowsAsync<AiUnavailableException>(() => svc.GenerateAsync("user-1", jobId, "cv", "Ada", Req("cover-letter"), "p", default));
    }

    [Fact]
    public async Task Another_users_job_is_not_found()
    {
        var (db, svc, _) = New("user-1");
        await using var _ = db;
        var otherJob = await SeedJobAsync(db, "user-2");

        var res = await svc.GenerateAsync("user-1", otherJob, "cv", "Ada", Req("job-analysis"), "p", default);
        Assert.Null(res);
    }
}
