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

namespace JobTrackerApi.Tests;

public sealed class ApplicationWorkspaceTests
{
    private static (JobTrackerContext db, ApplicationWorkspaceService 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 ApplicationWorkspaceService(db, new ApplicationChecklistService(db)));
    }

    // Milestone 2: the next action comes from the checklist, and every preparation item outranks the
    // later categories. Seeding the parts a test is not asserting on keeps the assertion about one rule.
    private static async Task CompletePreparationAsync(JobTrackerContext db, string owner, int jobId, params string[] except)
    {
        db.CareerProfiles.Add(new CareerProfile
        {
            OwnerUserId = owner,
            Experiences = { new CareerExperience { OwnerUserId = owner, Title = "Dev", Company = "Acme" } },
        });
        await db.SaveChangesAsync();

        var svc = new ApplicationChecklistService(db);
        var checklist = await svc.GetAsync(owner, jobId, default);
        foreach (var item in checklist!.Items.Where(i =>
                     i.Category == ChecklistCategories.Preparation &&
                     i.Status == ChecklistStatuses.Pending &&
                     !except.Contains(i.SystemKey)))
        {
            await svc.UpdateAsync(owner, jobId, item.Id, new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);
        }
    }

    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",
            Location = "Oslo",
            Description = "Needs .NET and SQL.",
            DateApplied = DateTime.UtcNow.AddDays(-3),
        };
        tweak?.Invoke(job);
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job;
    }

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

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

        Assert.NotNull(o);
        Assert.Equal("Backend Developer", o!.JobTitle);
        Assert.Equal("Acme", o.Company);
        Assert.Equal("Oslo", o.Location);
        Assert.Equal("Needs .NET and SQL.", o.Description);
        Assert.Equal(job.SavedAt, o.SavedAt);
        Assert.True(o.HasJobDescription);
        Assert.Null(o.Cv.VariantId);          // no variant attached yet
        Assert.False(o.HasCoverLetter);
        Assert.Equal(0, o.DocumentCount);
    }

    [Fact]
    public async Task Overview_separates_application_answers_from_human_notes()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1", j =>
        {
            j.Notes = "Ask about the platform team.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nI enjoy solving customer problems.\n<<<END_APPLICATION_ANSWER_DRAFT>>>";
            j.RecruiterMessageDraft = "Hello Maria";
        });

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

        Assert.Equal("Ask about the platform team.", o!.Notes);
        Assert.Equal("I enjoy solving customer problems.", o.ApplicationAnswerDraft);
        Assert.Equal("Hello Maria", o.RecruiterMessageDraft);
    }

    [Fact]
    public async Task Overview_surfaces_the_attached_cv_variant()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        db.CvVariants.Add(new CvVariant
        {
            OwnerUserId = "user-1",
            JobApplicationId = job.Id,
            PublicSlug = Guid.NewGuid().ToString("N"),
            Name = "Backend CV",
            SettingsJson = CvVariantSettingsJson.Serialize(new CvVariantSettings { ThemeId = "nordic" }),
            UpdatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();

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

        Assert.Equal("Backend CV", o!.Cv.VariantName);
        Assert.Equal("nordic", o.Cv.ThemeId);
    }

    [Fact]
    public async Task Overview_counts_documents_and_ai_history()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "cert.pdf", FilePath = "/tmp/a.pdf", FileType = "PDF" });
        db.AiInteractions.Add(new AiInteraction
        {
            OwnerUserId = "user-1", JobApplicationId = job.Id, Module = "job-analysis",
            Title = "Job analysis", Provider = "p", ResultJson = "{}", CreatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();

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

        Assert.Equal(1, o!.DocumentCount);
        Assert.Equal(1, o.AiInteractionCount);
        Assert.NotNull(o.LastAiAtUtc);
    }

    [Fact]
    public async Task Overview_returns_recent_activity_newest_first()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "Created", At = DateTime.Now.AddDays(-2) });
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "StatusChanged", NewValue = "Applied", At = DateTime.Now });
        await db.SaveChangesAsync();

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

        Assert.Equal(new[] { "StatusChanged", "Created" }, o!.RecentActivity.Select(a => a.Type));
    }

    // --- next recommended action: "the user should never ask what to do next" ---

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

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

        Assert.Equal("review-job-details", o!.NextStep!.Key);
        Assert.Equal("job-details", o.NextStep.Section);
    }

    [Fact]
    public async Task Next_step_walks_cv_then_cover_letter_then_documents()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1");
        await CompletePreparationAsync(db, "user-1", job.Id,
            "prepare-cv", "create-cover-letter", "attach-supporting-documents");

        Assert.Equal("prepare-cv", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);

        job.TailoredCvText = "tailored";
        await db.SaveChangesAsync();
        Assert.Equal("create-cover-letter", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);

        job.CoverLetterText = "Dear team";
        await db.SaveChangesAsync();
        Assert.Equal("attach-supporting-documents", (await svc.GetOverviewAsync("user-1", job.Id, default))!.NextStep!.Key);
    }

    [Fact]
    public async Task Next_step_reaches_interview_prep_once_preparation_is_done()
    {
        var (db, svc) = New("user-1");
        await using var _ = db;
        var job = await SeedAsync(db, "user-1", j =>
        {
            j.Status = "Interview";
            j.TailoredCvText = "tailored";
            j.CoverLetterText = "letter";
            j.FollowUpAt = DateTime.UtcNow.AddDays(3);
            j.NextAction = "Confirm the interview slot";
        });
        db.Attachments.Add(new Attachment { JobApplicationId = job.Id, FileName = "c.pdf", FilePath = "/tmp/c.pdf", FileType = "PDF" });
        await db.SaveChangesAsync();
        await CompletePreparationAsync(db, "user-1", job.Id);

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

        Assert.Equal("prepare-interview-notes", o!.NextStep!.Key);
        Assert.Equal("interview", o.NextStep.Section);
    }

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

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

        Assert.NotNull(o!.ChecklistProgress);
        Assert.True(o.ChecklistProgress!.Total > 0);
        // The advert is present, so that item auto-completed from the same signal readiness uses.
        Assert.True(o.ChecklistProgress.Completed > 0);
    }

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

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