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

namespace JobTrackerApi.Tests;

// Lifecycle events are derived from the TRANSITION, so one user action produces at most one event and
// an unchanged save produces none. JobEvent stays the only history store — these tests assert rows are
// added there and nowhere else.
public sealed class JobLifecycleEventTests
{
    private static (JobTrackerContext db, ApplicationTimelineService timeline) 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 ApplicationTimelineService(db));
    }

    private static async Task<JobApplication> SeedAsync(JobTrackerContext db, string owner, string status)
    {
        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 = "Dev", Status = status };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        return job;
    }

    private static async Task<List<string>> MoveAsync(JobTrackerContext db, JobApplication job, string to)
    {
        var old = job.Status;
        job.Status = to;
        JobLifecycleEvents.RecordStatusChange(db, job, old, DateTime.Now);
        await db.SaveChangesAsync();
        return await db.JobEvents.AsNoTracking().Select(e => e.Type).ToListAsync();
    }

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

        var types = await MoveAsync(db, job, "Interview");

        Assert.Contains("StatusChanged", types);
        Assert.Contains("InterviewScheduled", types);
        Assert.Equal(2, types.Count);
    }

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

        var types = await MoveAsync(db, job, "Offer");

        // An offer outranks the completed interview: one lifecycle event per action, the one that matters.
        Assert.Contains("OfferReceived", types);
        Assert.DoesNotContain("InterviewCompleted", types);
    }

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

        var types = await MoveAsync(db, job, "Rejected");

        Assert.Contains("InterviewCompleted", types);
    }

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

        var types = await MoveAsync(db, job, "Applied");

        Assert.Contains("StatusChanged", types);
        Assert.DoesNotContain("InterviewCompleted", types);
    }

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

        await MoveAsync(db, job, "Offer");
        // Saving the same status again is not a new action, so nothing more is recorded.
        var types = await MoveAsync(db, job, "Offer");

        Assert.Single(types, t => t == "OfferReceived");
        Assert.Single(types, t => t == "StatusChanged");
    }

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

        var types = await MoveAsync(db, job, "Applied");

        Assert.Empty(types);
    }

    [Fact]
    public async Task Completing_a_follow_up_checklist_item_records_it_once()
    {
        var (db, _) = New("user-1");
        await using var _d = db;
        var job = await SeedAsync(db, "user-1", "Applied");
        var svc = new ApplicationChecklistService(db);
        var checklist = await svc.GetAsync("user-1", job.Id, default);
        var followUp = checklist!.Items.First(i =>
            i.Category == ChecklistCategories.FollowUp && i.Status == ChecklistStatuses.Pending);

        await svc.UpdateAsync("user-1", job.Id, followUp.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);
        // Re-saving an already-done item must not emit a second event.
        await svc.UpdateAsync("user-1", job.Id, followUp.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);

        Assert.Equal(1, await db.JobEvents.CountAsync(e => e.Type == "FollowUpCompleted"));
    }

    [Fact]
    public async Task Completing_a_non_follow_up_item_records_nothing()
    {
        var (db, _) = New("user-1");
        await using var _d = db;
        var job = await SeedAsync(db, "user-1", "Applied");
        var svc = new ApplicationChecklistService(db);
        var checklist = await svc.GetAsync("user-1", job.Id, default);
        var preparation = checklist!.Items.First(i =>
            i.Category == ChecklistCategories.Preparation && i.Status == ChecklistStatuses.Pending);

        await svc.UpdateAsync("user-1", job.Id, preparation.Id,
            new ChecklistItemInput(null, null, null, ChecklistStatuses.Done, null), default);

        Assert.Equal(0, await db.JobEvents.CountAsync(e => e.Type == "FollowUpCompleted"));
    }

    [Fact]
    public async Task The_timeline_renders_the_emitted_events()
    {
        var (db, timeline) = New("user-1");
        await using var _d = db;
        var job = await SeedAsync(db, "user-1", "Applied");

        await MoveAsync(db, job, "Interview");
        await MoveAsync(db, job, "Offer");

        var result = await timeline.GetAsync("user-1", job.Id, null, false, default);
        var summaries = result!.Days.SelectMany(d => d.Events).Select(e => e.Summary).ToList();

        Assert.Contains("Interview scheduled — Interview", summaries);
        Assert.Contains("Offer received", summaries);
        // Interviews and offers belong on the milestone spine.
        Assert.Contains(result.Milestones, m => m.Type == "InterviewScheduled");
        Assert.Contains(result.Milestones, m => m.Type == "OfferReceived");
    }
}
