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

namespace JobTrackerApi.Tests;

// Phase 5.5 — Interview preparation and follow-up. The properties under test: prep content belongs to
// the user and nothing regenerates it, follow-ups reuse the existing FollowUpAt + checklist rather
// than a second reminder system, and the timeline reads the new lifecycle events.
public sealed class InterviewPrepTests
{
    private static (JobTrackerContext db, InterviewPrepService prep, 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 InterviewPrepService(db), new ApplicationTimelineService(db));
    }

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

    // ---------- Part 1: interview preparation ----------

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

        await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Question, "What does success look like?", null, null, null), default);
        await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.CompanyResearch, "Funding history", "Series B in 2025.", null, null), default);
        await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Technical, "System design", null, null, null), default);

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

        Assert.Equal(
            new[] { InterviewPrepCategories.CompanyResearch, InterviewPrepCategories.Technical, InterviewPrepCategories.Question },
            result!.Groups.Select(g => g.Category));
        Assert.Equal("Company research", result.Groups[0].Label);
    }

    [Fact]
    public async Task Marking_items_prepared_drives_the_progress_percentage()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        var a = await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(null, "One", null, null, null), default);
        await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(null, "Two", null, null, null), default);

        await prep.UpdateAsync("user-1", job.Id, a!.Id, new InterviewPrepInput(null, null, null, null, true), default);
        var result = await prep.GetAsync("user-1", job.Id, default);

        Assert.Equal(2, result!.Total);
        Assert.Equal(1, result.Prepared);
        Assert.Equal(50, result.Percent);
    }

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

        var item = await prep.AddAsync("user-1", job.Id,
            new InterviewPrepInput(InterviewPrepCategories.Behavioural, "Tell me about a conflict", "Draft answer", InterviewPrepSources.Ai, null), default);

        Assert.Equal(InterviewPrepSources.Ai, item!.Source);

        var edited = await prep.UpdateAsync("user-1", job.Id, item.Id,
            new InterviewPrepInput(null, null, "My own answer", null, null), default);

        Assert.Equal("My own answer", edited!.Content);
    }

    [Fact]
    public async Task Generating_ai_history_does_not_create_prep_items()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        db.AiInteractions.Add(new AiInteraction
        {
            OwnerUserId = "user-1", JobApplicationId = job.Id, Module = "interview",
            Title = "Interview prep", Provider = "p", ResultJson = """{"text":"Likely questions..."}""",
            CreatedAtUtc = DateTimeOffset.UtcNow,
        });
        await db.SaveChangesAsync();

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

        // A suggestion is history until the user accepts it. Nothing appears in their prep by itself.
        Assert.Equal(0, result!.Total);
        Assert.Equal(1, result.AiSuggestionCount);
    }

    [Fact]
    public async Task Prep_items_are_deletable_and_scoped_to_their_owner()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var mine = await SeedJobAsync(db, "user-1");
        var theirs = await SeedJobAsync(db, "user-2");
        var item = await prep.AddAsync("user-1", mine.Id, new InterviewPrepInput(null, "Mine", null, null, null), default);

        Assert.Null(await prep.GetAsync("user-1", theirs.Id, default));
        Assert.Null(await prep.AddAsync("user-1", theirs.Id, new InterviewPrepInput(null, "Sneak", null, null, null), default));
        Assert.False(await prep.DeleteAsync("user-1", theirs.Id, item!.Id, default));

        Assert.True(await prep.DeleteAsync("user-1", mine.Id, item.Id, default));
        Assert.Equal(0, (await prep.GetAsync("user-1", mine.Id, default))!.Total);
    }

    [Fact]
    public async Task Prep_never_writes_to_the_career_profile()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        db.CareerProfiles.Add(new CareerProfile
        {
            OwnerUserId = "user-1",
            Experiences = { new CareerExperience { OwnerUserId = "user-1", Title = "Dev", BulletsJson = """["Original"]""" } },
        });
        await db.SaveChangesAsync();
        var before = await db.CareerProfiles.AsNoTracking().Include(p => p.Experiences).FirstAsync();

        await prep.AddAsync("user-1", job.Id, new InterviewPrepInput(InterviewPrepCategories.Star, "A time I led", "Content", InterviewPrepSources.Ai, null), default);

        var after = await db.CareerProfiles.AsNoTracking().Include(p => p.Experiences).FirstAsync();
        Assert.Equal(before.Version, after.Version);
        Assert.Equal(before.Experiences[0].BulletsJson, after.Experiences[0].BulletsJson);
    }

    // ---------- Part 4: follow-up ----------

    [Fact]
    public async Task Setting_a_follow_up_updates_the_existing_field_and_records_an_event()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        var due = DateTime.Today.AddDays(7);

        var result = await prep.SetFollowUpAsync("user-1", job.Id, due, "Chase the recruiter", default);

        Assert.Equal(due, result!.FollowUpAt);
        Assert.Equal("Chase the recruiter", result.NextAction);
        // The same field the reminder service and RulesEngine already read — not a parallel store.
        Assert.Equal(due, (await db.JobApplications.AsNoTracking().FirstAsync(j => j.Id == job.Id)).FollowUpAt);
        Assert.Equal("FollowUpSet", (await db.JobEvents.AsNoTracking().SingleAsync()).Type);
    }

    [Fact]
    public async Task Follow_up_counts_open_checklist_tasks_rather_than_owning_them()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        db.ApplicationChecklistItems.Add(new ApplicationChecklistItem
        {
            OwnerUserId = "user-1", JobApplicationId = job.Id, Title = "Chase",
            Category = ChecklistCategories.FollowUp, Status = ChecklistStatuses.Pending,
        });
        db.ApplicationChecklistItems.Add(new ApplicationChecklistItem
        {
            OwnerUserId = "user-1", JobApplicationId = job.Id, Title = "Done one",
            Category = ChecklistCategories.FollowUp, Status = ChecklistStatuses.Done,
        });
        await db.SaveChangesAsync();

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

        Assert.Equal(1, result!.OpenFollowUpTasks);
        // No follow-up table was created — the tasks live in the checklist.
        Assert.Equal(2, await db.ApplicationChecklistItems.CountAsync());
    }

    [Fact]
    public async Task Clearing_a_follow_up_is_recorded_too()
    {
        var (db, prep, _) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1", j => j.FollowUpAt = DateTime.Today.AddDays(3));

        var result = await prep.SetFollowUpAsync("user-1", job.Id, null, null, default);

        Assert.Null(result!.FollowUpAt);
        Assert.Equal(1, await db.JobEvents.CountAsync());
    }

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

        Assert.Null(await prep.GetFollowUpAsync("user-1", other.Id, default));
        Assert.Null(await prep.SetFollowUpAsync("user-1", other.Id, DateTime.Today, null, default));
    }

    // ---------- Part 5: timeline integration ----------

    [Fact]
    public async Task Timeline_reads_the_new_lifecycle_events()
    {
        var (db, _, timeline) = New("user-1");
        await using var _d = db;
        var job = await SeedJobAsync(db, "user-1");
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "InterviewScheduled", NewValue = "2026-08-01", At = DateTime.Now.AddDays(-2) });
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "InterviewCompleted", At = DateTime.Now.AddDays(-1) });
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "OfferReceived", At = DateTime.Now });
        db.JobEvents.Add(new JobEvent { JobApplicationId = job.Id, Type = "FollowUpCompleted", At = DateTime.Now });
        await db.SaveChangesAsync();

        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 for 1 August 2026", summaries);
        Assert.Contains("Interview completed", summaries);
        Assert.Contains("Offer received", summaries);
        Assert.Contains("Follow-up completed", summaries);

        // Interviews and offers are milestones; a completed follow-up is routine.
        Assert.Equal(3, result.Milestones.Count);
        Assert.DoesNotContain(result.Milestones, m => m.Type == "FollowUpCompleted");
    }
}
