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

namespace JobTrackerApi.Tests;

// Phase 5 Milestone 2. The point of these tests is that the checklist is ONE system: system items seed
// from the same signals readiness reads, auto-complete when those signals are satisfied, and stay under
// the user's control after that.
public sealed class ApplicationChecklistTests
{
    private static (JobTrackerContext db, ApplicationChecklistService svc) 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);
        return (db, new ApplicationChecklistService(db));
    }

    private static async Task<JobApplication> SeedAsync(JobTrackerContext db, string owner, Action<JobApplication>? tweak = null)
    {
        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 = "Backend Developer",
            Status = "Applied",
            Description = "Needs .NET and SQL.",
            DateApplied = DateTime.UtcNow.AddDays(-3),
        };
        tweak?.Invoke(job);
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job;
    }

    private static ChecklistItemDto Item(ChecklistDto checklist, string systemKey) =>
        checklist.Items.Single(i => i.SystemKey == systemKey);

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

        var checklist = await svc.GetAsync("user-1", job.Id, default);

        Assert.NotNull(checklist);
        Assert.All(checklist!.Items, i => Assert.True(i.IsSystemGenerated));
        Assert.Contains(checklist.Items, i => i.SystemKey == "prepare-cv");
        Assert.Contains(checklist.Items, i => i.SystemKey == "confirm-submitted");
        Assert.Contains(checklist.Items, i => i.SystemKey == "research-company");
    }

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

        var first = await svc.GetAsync("user-1", job.Id, default);
        var second = await svc.GetAsync("user-1", job.Id, default);

        Assert.Equal(first!.Items.Count, second!.Items.Count);
        Assert.Equal(first.Items.Count, await db.ApplicationChecklistItems.CountAsync());
    }

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

        var before = await svc.GetAsync("user-1", job.Id, default);
        Assert.Equal(ChecklistStatuses.Done, Item(before!, "review-job-details").Status);   // advert present
        Assert.Equal(ChecklistStatuses.Pending, Item(before!, "prepare-cv").Status);

        job.TailoredCvText = "tailored";
        await db.SaveChangesAsync();

        var after = await svc.GetAsync("user-1", job.Id, default);
        var cv = Item(after!, "prepare-cv");
        Assert.Equal(ChecklistStatuses.Done, cv.Status);
        Assert.True(cv.IsAutoCompleted);
    }

    [Fact]
    public async Task An_auto_completed_item_reopens_when_its_signal_goes_away()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1", j => j.CoverLetterText = "Dear team");

        Assert.Equal(ChecklistStatuses.Done, Item((await svc.GetAsync("user-1", job.Id, default))!, "create-cover-letter").Status);

        job.CoverLetterText = null;
        job.HasCoverLetter = false;
        await db.SaveChangesAsync();

        Assert.Equal(ChecklistStatuses.Pending, Item((await svc.GetAsync("user-1", job.Id, default))!, "create-cover-letter").Status);
    }

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

        var portfolio = Item((await svc.GetAsync("user-1", job.Id, default))!, "attach-portfolio");
        await svc.UpdateAsync("user-1", job.Id, portfolio.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);

        // The portfolio signal is still false, but the user said it is done — that must stick.
        var after = Item((await svc.GetAsync("user-1", job.Id, default))!, "attach-portfolio");
        Assert.Equal(ChecklistStatuses.Done, after.Status);
        Assert.False(after.IsAutoCompleted);
    }

    [Fact]
    public async Task Custom_items_can_be_added_and_deleted()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        await svc.GetAsync("user-1", job.Id, default);

        var created = await svc.AddAsync("user-1", job.Id,
            new ChecklistItemInput("Ask Sara for a referral", "She worked there until 2025.", null, null, null), default);

        Assert.NotNull(created);
        Assert.False(created!.IsSystemGenerated);
        Assert.Equal(ChecklistCategories.Custom, created.Category);

        Assert.True(await svc.DeleteAsync("user-1", job.Id, created.Id, default));
        var after = await svc.GetAsync("user-1", job.Id, default);
        Assert.DoesNotContain(after!.Items, i => i.Id == created.Id);
    }

    [Fact]
    public async Task Deleting_a_system_item_dismisses_it_instead_of_resurrecting_it()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        var portfolio = Item((await svc.GetAsync("user-1", job.Id, default))!, "attach-portfolio");

        Assert.True(await svc.DeleteAsync("user-1", job.Id, portfolio.Id, default));

        // A hard delete would be undone by the next seed, so removal means "dismissed" for system items.
        var after = await svc.GetAsync("user-1", job.Id, default);
        Assert.Equal(ChecklistStatuses.Dismissed, Item(after!, "attach-portfolio").Status);
    }

    [Fact]
    public async Task Dismissed_items_leave_the_progress_denominator()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        var before = await svc.GetAsync("user-1", job.Id, default);
        var total = before!.Progress.Total;

        await svc.DeleteAsync("user-1", job.Id, Item(before, "attach-portfolio").Id, default);

        var after = await svc.GetAsync("user-1", job.Id, default);
        Assert.Equal(total - 1, after!.Progress.Total);
        Assert.Equal(1, after.Progress.Dismissed);
    }

    [Fact]
    public async Task Reordering_persists_the_new_order()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        var checklist = await svc.GetAsync("user-1", job.Id, default);
        var preparation = checklist!.Items.Where(i => i.Category == ChecklistCategories.Preparation).ToList();
        var reversed = preparation.Select(i => i.Id).Reverse().ToList();

        await svc.ReorderAsync("user-1", job.Id, reversed, default);

        var after = await svc.GetAsync("user-1", job.Id, default);
        var afterPreparation = after!.Items.Where(i => i.Category == ChecklistCategories.Preparation).Select(i => i.Id).ToList();
        Assert.Equal(reversed, afterPreparation);
    }

    [Fact]
    public async Task Next_pending_follows_the_category_priority()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        var checklist = await svc.GetAsync("user-1", job.Id, default);

        var next = ApplicationChecklistService.NextPending(checklist!);

        // Preparation outranks submission, follow-up, interview and custom.
        Assert.Equal(ChecklistCategories.Preparation, next!.Category);
    }

    [Fact]
    public async Task A_custom_item_can_become_the_next_action_once_the_system_items_are_done()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        var checklist = await svc.GetAsync("user-1", job.Id, default);
        foreach (var item in checklist!.Items.Where(i => i.Status == ChecklistStatuses.Pending))
        {
            await svc.UpdateAsync("user-1", job.Id, item.Id, new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);
        }
        var custom = await svc.AddAsync("user-1", job.Id, new ChecklistItemInput("Chase the recruiter", null, null, null, null), default);

        var next = ApplicationChecklistService.NextPending((await svc.GetAsync("user-1", job.Id, default))!);

        Assert.Equal(custom!.Id, next!.Id);
    }

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

        var first = await svc.SyncLearningRecommendationsAsync("user-1", job.Id, ["Kubernetes", "AWS"], default);
        var kubernetes = first.Single(item => item.Keyword == "Kubernetes");
        await svc.UpdateAsync("user-1", job.Id, kubernetes.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);

        await svc.SyncLearningRecommendationsAsync("user-1", job.Id, [], default);
        var reopened = await svc.SyncLearningRecommendationsAsync("user-1", job.Id, ["Kubernetes", "AWS"], default);

        Assert.Equal(ChecklistStatuses.Done, reopened.Single(item => item.Keyword == "Kubernetes").Status);
        Assert.Equal(ChecklistStatuses.Pending, reopened.Single(item => item.Keyword == "AWS").Status);
    }

    [Fact]
    public async Task Interview_prep_is_only_outstanding_at_the_interview_stage()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var applied = await SeedAsync(db, "user-1");
        var interviewing = await SeedAsync(db, "user-1", j => j.Status = "Interview");

        Assert.Equal(ChecklistStatuses.Done,
            Item((await svc.GetAsync("user-1", applied.Id, default))!, "prepare-interview-notes").Status);
        Assert.Equal(ChecklistStatuses.Pending,
            Item((await svc.GetAsync("user-1", interviewing.Id, default))!, "prepare-interview-notes").Status);
    }

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

        Assert.Null(await svc.GetAsync("user-1", other.Id, default));
        Assert.Null(await svc.AddAsync("user-1", other.Id, new ChecklistItemInput("Sneak", null, null, null, null), default));
        Assert.False(await svc.DeleteAsync("user-1", other.Id, 1, default));
        Assert.Empty(await svc.SyncLearningRecommendationsAsync("user-1", other.Id, ["Kubernetes"], default));
    }

    [Fact]
    public async Task An_items_owner_is_checked_before_it_can_be_updated()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var mine = await SeedAsync(db, "user-1");
        var theirs = await SeedAsync(db, "user-2");
        var theirItem = new ApplicationChecklistItem
        {
            OwnerUserId = "user-2", JobApplicationId = theirs.Id, Title = "Theirs", Category = ChecklistCategories.Custom,
        };
        db.ApplicationChecklistItems.Add(theirItem);
        await db.SaveChangesAsync();

        Assert.Null(await svc.UpdateAsync("user-1", mine.Id, theirItem.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default));
    }
}
