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

namespace JobTrackerApi.Tests;

// Interview AI context integration. The interview module now sees what the workspace already computed
// — requirements, matched skills, gaps, relevant experience — so its questions are about THIS
// application rather than generic. The context is deterministic and read-only, so this adds no second
// AI pipeline and cannot change the user's data.
public sealed class InterviewAiContextTests
{
    private sealed class FakeAi : ISummarizerService
    {
        public string? Next = "## Likely questions\nSomething specific.";
        public string? LastText;
        public int Calls;

        public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40)
        {
            Calls++;
            LastText = text;
            return Task.FromResult(Next);
        }

        public Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30) => Task.FromResult(Next);
        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 const string Advert = """
        Senior Backend Developer, full-time, Oslo.

        We expect:
        - Strong experience with C# and .NET
        - Solid SQL knowledge
        - Experience with Kubernetes in production
        """;

    private static (JobTrackerContext db, AiWorkspaceService svc, FakeAi ai) New(string userId, bool withIntelligence = true)
    {
        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();
        var intelligence = withIntelligence ? new ApplicationIntelligenceService(db, new JobCvMatchService()) : null;
        return (db, new AiWorkspaceService(db, ai, intelligence), ai);
    }

    private static async Task<JobApplication> 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 Backend Developer",
            Status = "Interview",
            Description = Advert,
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job;
    }

    private static async Task SeedProfileAsync(JobTrackerContext db, string owner)
    {
        db.CareerProfiles.Add(new CareerProfile
        {
            OwnerUserId = owner,
            Experiences =
            {
                new CareerExperience
                {
                    OwnerUserId = owner, Title = "Backend Developer", Company = "Initech", Start = "2021", IsCurrent = true,
                    BulletsJson = """["Built services in C# and .NET","Owned the SQL migration programme"]""",
                },
            },
            Projects =
            {
                new CareerProject { OwnerUserId = owner, Name = "Deploy pipeline", Role = "Author", BulletsJson = """["C# tooling"]""" },
            },
        });
        await db.SaveChangesAsync();
    }

    private static async Task AttachCvAsync(JobTrackerContext db, string owner, int jobId)
    {
        db.CvVariants.Add(new CvVariant
        {
            OwnerUserId = owner,
            JobApplicationId = jobId,
            Name = "Backend CV",
            PublicSlug = Guid.NewGuid().ToString("N"),
            SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings()),
            Version = 1,
            CreatedAtUtc = DateTimeOffset.UtcNow,
            UpdatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();
    }

    private static AiGenerateRequest Interview() => new("interview", null, null);

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

        await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        Assert.Contains("APPLICATION INTELLIGENCE", ai.LastText);
        Assert.Contains("Seniority: Senior", ai.LastText);
        Assert.Contains("Key requirements", ai.LastText);
        Assert.Contains("Technologies in the advert", ai.LastText);
    }

    [Fact]
    public async Task Interview_generation_includes_the_career_match_context_and_the_gaps()
    {
        var (db, svc, ai) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        await SeedProfileAsync(db, "user-1");
        await AttachCvAsync(db, "user-1", job.Id);

        await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        Assert.Contains("Match score:", ai.LastText);
        Assert.Contains("Skills the candidate demonstrably has", ai.LastText);
        // The gaps are the whole point — an interviewer probes what is missing.
        Assert.Contains("Gaps the candidate must be ready to address", ai.LastText);
        Assert.Contains("Kubernetes", ai.LastText);
        Assert.Contains("Most relevant experience", ai.LastText);
        Assert.Contains("Backend Developer", ai.LastText);
    }

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

        foreach (var module in new[] { "job-analysis", "career-match", "application-review" })
        {
            await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", new AiGenerateRequest(module, null, null), "test", default);
            Assert.DoesNotContain("APPLICATION INTELLIGENCE", ai.LastText);
        }
    }

    [Fact]
    public async Task Generation_still_works_without_the_intelligence_service()
    {
        // The dependency is optional, so an older construction path degrades to the previous prompt
        // rather than failing.
        var (db, svc, ai) = New("user-1", withIntelligence: false);
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");

        var interaction = await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        Assert.NotNull(interaction);
        Assert.DoesNotContain("APPLICATION INTELLIGENCE", ai.LastText);
        Assert.Contains("JOB ADVERT", ai.LastText);
    }

    [Fact]
    public async Task Context_is_scoped_to_the_requesting_user()
    {
        var (db, svc, ai) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        await SeedProfileAsync(db, "user-2");   // another user's profile must never be scored in

        await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        Assert.DoesNotContain("Initech", ai.LastText);
        Assert.DoesNotContain("Match score:", ai.LastText);
    }

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

        Assert.Null(await svc.GenerateAsync("user-1", other.Id, "profile text", "Ada", Interview(), "test", default));
        Assert.Equal(0, ai.Calls);
    }

    [Fact]
    public async Task Generation_writes_history_only_and_never_the_users_content()
    {
        var (db, svc, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        await SeedProfileAsync(db, "user-1");
        var profileBefore = await db.CareerProfiles.AsNoTracking().Include(p => p.Experiences).FirstAsync();
        var descriptionBefore = job.Description;

        await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        // The only new row is the AiInteraction. No prep item is created — the user must accept one.
        Assert.Equal(1, await db.AiInteractions.CountAsync());
        Assert.Equal(0, await db.InterviewPrepItems.CountAsync());

        var profileAfter = await db.CareerProfiles.AsNoTracking().Include(p => p.Experiences).FirstAsync();
        Assert.Equal(profileBefore.Version, profileAfter.Version);
        Assert.Equal(profileBefore.Experiences[0].BulletsJson, profileAfter.Experiences[0].BulletsJson);
        Assert.Equal(descriptionBefore, (await db.JobApplications.AsNoTracking().FirstAsync(j => j.Id == job.Id)).Description);
    }

    [Fact]
    public async Task Existing_prep_items_are_never_overwritten_by_generation()
    {
        var (db, svc, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        var prep = new InterviewPrepService(db);
        var mine = await prep.AddAsync("user-1", job.Id,
            new InterviewPrepInput(InterviewPrepCategories.Star, "My STAR example", "My own words", null, null), default);

        await svc.GenerateAsync("user-1", job.Id, "profile text", "Ada", Interview(), "test", default);

        var after = await prep.GetAsync("user-1", job.Id, default);
        Assert.Equal(1, after!.Total);
        Assert.Equal("My own words", after.Groups[0].Items[0].Content);
        Assert.Equal(mine!.Id, after.Groups[0].Items[0].Id);
    }
}
