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

namespace JobTrackerApi.Tests;

// Phase 5.4 — Application Assets. The properties under test are the architecture rules: the flow runs
// CareerProfile -> CvVariant -> application output and never back, versions are never destroyed, and
// nothing is reachable across tenants.
public sealed class ApplicationAssetsTests
{
    private static (JobTrackerContext db, ApplicationAssetsService assets) 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);

        // Only ListAsync is used here — attaching, creating, previewing and exporting all stay in
        // CvVariantService, which has its own tests. Mocking it keeps these tests about association.
        var variants = new Mock<ICvVariantService>();
        variants
            .Setup(s => s.ListAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync((string owner, CancellationToken _) => db.CvVariants
                .Where(v => v.OwnerUserId == owner)
                .Select(v => new CvVariantSummary(v.Id, v.Name, "nordic", "en", v.PublicSlug, v.IsPublic, v.Version, v.JobApplicationId, v.UpdatedAtUtc, null, null))
                .ToList());

        var intelligence = new ApplicationIntelligenceService(db, new JobCvMatchService());
        return (db, new ApplicationAssetsService(db, variants.Object, intelligence));
    }

    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 = "Applied",
            Description = "We need strong C# and .NET experience, plus SQL and Docker. You will build REST APIs.",
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job;
    }

    private static async Task<CvVariant> SeedVariantAsync(JobTrackerContext db, string owner, string name, int? jobId = null)
    {
        var variant = new CvVariant
        {
            OwnerUserId = owner,
            Name = name,
            JobApplicationId = jobId,
            PublicSlug = Guid.NewGuid().ToString("N"),
            SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings { ThemeId = "nordic" }),
            CreatedAtUtc = DateTimeOffset.UtcNow,
            UpdatedAtUtc = DateTimeOffset.UtcNow,
        };
        db.CvVariants.Add(variant);
        await db.SaveChangesAsync();
        return variant;
    }

    // ---------- Part 1: CV variant association ----------

    [Fact]
    public async Task Attaching_a_variant_points_the_application_at_it()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        var variant = await SeedVariantAsync(db, "user-1", "Backend CV");

        var cv = await assets.AttachVariantAsync("user-1", job.Id, variant.Id, default);

        Assert.Equal(variant.Id, cv!.AttachedVariantId);
        Assert.Equal("Backend CV", cv.AttachedVariantName);
        Assert.Equal("nordic", cv.AttachedThemeId);
    }

    [Fact]
    public async Task Attaching_a_second_variant_replaces_the_first_rather_than_stacking()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        var first = await SeedVariantAsync(db, "user-1", "First");
        var second = await SeedVariantAsync(db, "user-1", "Second");

        await assets.AttachVariantAsync("user-1", job.Id, first.Id, default);
        var cv = await assets.AttachVariantAsync("user-1", job.Id, second.Id, default);

        Assert.Equal(second.Id, cv!.AttachedVariantId);
        // The replaced variant is detached, not deleted — it is still the user's to reuse.
        var stillThere = await db.CvVariants.FirstAsync(v => v.Id == first.Id);
        Assert.Null(stillThere.JobApplicationId);
    }

    [Fact]
    public async Task Detaching_leaves_the_variant_intact()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        var variant = await SeedVariantAsync(db, "user-1", "Backend CV", null);
        await assets.AttachVariantAsync("user-1", job.Id, variant.Id, default);

        var cv = await assets.AttachVariantAsync("user-1", job.Id, null, default);

        Assert.Null(cv!.AttachedVariantId);
        Assert.Equal(1, await db.CvVariants.CountAsync());
    }

    [Fact]
    public async Task Another_users_variant_cannot_be_attached()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        var theirs = await SeedVariantAsync(db, "user-2", "Not yours");

        Assert.Null(await assets.AttachVariantAsync("user-1", job.Id, theirs.Id, default));
    }

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

        Assert.Null(await assets.GetCvAsync("user-1", other.Id, default));
    }

    // ---------- Part 2: tailoring ----------

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

        var plan = await assets.GetTailoringPlanAsync("user-1", job.Id, default);

        Assert.NotNull(plan);
        Assert.False(plan!.HasCareerProfile);
        Assert.Contains(plan.Suggestions, s => s.Kind == "include-keywords");
    }

    [Fact]
    public async Task Tailoring_never_writes_to_the_career_profile_or_the_variant()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        var variant = await SeedVariantAsync(db, "user-1", "Backend CV", job.Id);
        db.CareerProfiles.Add(new CareerProfile
        {
            OwnerUserId = "user-1",
            Experiences = { new CareerExperience { OwnerUserId = "user-1", Title = "Dev", Company = "Initech", BulletsJson = """["C# and .NET"]""" } },
        });
        await db.SaveChangesAsync();
        var settingsBefore = variant.SettingsJson;
        var versionBefore = variant.Version;
        var profileVersionBefore = (await db.CareerProfiles.FirstAsync()).Version;

        var plan = await assets.GetTailoringPlanAsync("user-1", job.Id, default);

        Assert.NotEmpty(plan!.Suggestions);
        var variantAfter = await db.CvVariants.AsNoTracking().FirstAsync(v => v.Id == variant.Id);
        Assert.Equal(settingsBefore, variantAfter.SettingsJson);
        Assert.Equal(versionBefore, variantAfter.Version);
        Assert.Equal(profileVersionBefore, (await db.CareerProfiles.AsNoTracking().FirstAsync()).Version);
    }

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

        Assert.Null(await assets.GetTailoringPlanAsync("user-1", other.Id, default));
    }

    // ---------- Part 3: cover letter ----------

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

        var result = await assets.SaveCoverLetterAsync("user-1", job.Id, "Dear team", CoverLetterSources.Manual, null, default);

        Assert.Equal("Dear team", result!.Text);
        Assert.Equal(1, result.CurrentVersion);
        Assert.Single(result.Versions);
        Assert.True(result.Versions[0].IsCurrent);
        Assert.Equal("Dear team", (await db.JobApplications.AsNoTracking().FirstAsync(j => j.Id == job.Id)).CoverLetterText);
    }

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

        await assets.SaveCoverLetterAsync("user-1", job.Id, "First draft", CoverLetterSources.Manual, null, default);
        await assets.SaveCoverLetterAsync("user-1", job.Id, "Second draft", CoverLetterSources.Ai, "improve", default);
        var result = await assets.SaveCoverLetterAsync("user-1", job.Id, "Third draft", CoverLetterSources.Manual, null, default);

        Assert.Equal(3, result!.CurrentVersion);
        Assert.Equal(3, result.Versions.Count);
        Assert.Equal("improve", result.Versions.Single(v => v.Version == 2).AiAction);
        Assert.Equal(CoverLetterSources.Ai, result.Versions.Single(v => v.Version == 2).Source);
    }

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

        await assets.SaveCoverLetterAsync("user-1", job.Id, "Same text", CoverLetterSources.Manual, null, default);
        var result = await assets.SaveCoverLetterAsync("user-1", job.Id, "Same text", CoverLetterSources.Manual, null, default);

        Assert.Single(result!.Versions);
    }

    [Fact]
    public async Task Restoring_brings_back_old_text_without_destroying_the_newer_version()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        await assets.SaveCoverLetterAsync("user-1", job.Id, "The good draft", CoverLetterSources.Manual, null, default);
        await assets.SaveCoverLetterAsync("user-1", job.Id, "The AI ruined it", CoverLetterSources.Ai, "shorten", default);

        var result = await assets.RestoreCoverLetterAsync("user-1", job.Id, 1, default);

        Assert.Equal("The good draft", result!.Text);
        // Restore is additive: v1 and v2 both survive, and the restore is v3.
        Assert.Equal(3, result.CurrentVersion);
        Assert.Equal(3, result.Versions.Count);
        Assert.Equal(CoverLetterSources.Restore, result.Versions.Single(v => v.Version == 3).Source);
        Assert.Equal("The AI ruined it", (await db.CoverLetterVersions.AsNoTracking().FirstAsync(v => v.Version == 2)).Text);
    }

    [Fact]
    public async Task Restoring_a_version_that_does_not_exist_is_a_miss()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        await assets.SaveCoverLetterAsync("user-1", job.Id, "Only draft", CoverLetterSources.Manual, null, default);

        Assert.Null(await assets.RestoreCoverLetterAsync("user-1", job.Id, 99, default));
    }

    [Fact]
    public async Task Cover_letter_history_is_scoped_to_its_owner()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var mine = await SeedJobAsync(db, "user-1");
        var theirs = await SeedJobAsync(db, "user-2");
        await assets.SaveCoverLetterAsync("user-1", mine.Id, "Mine", CoverLetterSources.Manual, null, default);

        Assert.Null(await assets.GetCoverLetterAsync("user-1", theirs.Id, default));
        Assert.Null(await assets.SaveCoverLetterAsync("user-1", theirs.Id, "Sneak", CoverLetterSources.Manual, null, default));
        Assert.Null(await assets.RestoreCoverLetterAsync("user-1", theirs.Id, 1, default));
    }

    [Fact]
    public async Task An_ai_draft_only_becomes_a_version_when_the_user_saves_it()
    {
        var (db, assets) = New("user-1");
        await using var _ = db;
        var job = await SeedJobAsync(db, "user-1");
        // An AI generation on its own is an AiInteraction — history, not the user's cover letter.
        db.AiInteractions.Add(new AiInteraction
        {
            OwnerUserId = "user-1", JobApplicationId = job.Id, Module = "cover-letter",
            Title = "Cover letter", Provider = "p", ResultJson = """{"text":"AI suggestion"}""",
            CreatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();

        var before = await assets.GetCoverLetterAsync("user-1", job.Id, default);

        Assert.Null(before!.Text);
        Assert.Empty(before.Versions);
        Assert.Equal(1, before.AiSuggestionCount);
    }
}
