using System;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class JobPipelineTests
{
    [Theory]
    [InlineData("applied", "Applied")]
    [InlineData("APPLIED", "Applied")]
    [InlineData("  Offer  ", "Offer")]
    [InlineData("Interviewing", "Interview")]
    [InlineData("interviews", "Interview")]
    [InlineData("declined", "Rejected")]
    [InlineData("no response", "Ghosted")]
    public void Normalize_canonicalizes_casing_and_synonyms(string input, string expected)
        => Assert.Equal(expected, JobPipeline.Normalize(input));

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData(null)]
    public void Normalize_empty_becomes_default(string? input)
        => Assert.Equal("Applied", JobPipeline.Normalize(input));

    [Fact]
    public void Normalize_preserves_unknown_custom_status()
        => Assert.Equal("Take-home assignment", JobPipeline.Normalize("  Take-home assignment  "));

    [Fact]
    public void Stages_are_ordered_and_unique()
    {
        var orders = JobPipeline.Stages.Select(s => s.Order).ToList();
        Assert.Equal(orders.OrderBy(x => x), orders);
        Assert.Equal(orders.Count, orders.Distinct().Count());
    }

    [Fact]
    public void OrderOf_sorts_canonical_before_custom()
    {
        Assert.True(JobPipeline.OrderOf("Applied") < JobPipeline.OrderOf("Offer"));
        Assert.True(JobPipeline.OrderOf("Offer") < JobPipeline.OrderOf("Custom stage"));
        Assert.Equal(JobPipeline.OrderOf("Interview"), JobPipeline.OrderOf("Interviewing"));
    }

    [Fact]
    public void IsCanonical_only_true_for_known_stages()
    {
        Assert.True(JobPipeline.IsCanonical("Offer"));
        Assert.True(JobPipeline.IsCanonical("offer"));
        Assert.False(JobPipeline.IsCanonical("Interviewing")); // synonym, not canonical
        Assert.False(JobPipeline.IsCanonical("Whatever"));
    }

    // --- Pre-application (Prospect) stages ------------------------------------------------

    [Theory]
    [InlineData("bookmarked", "Saved")]
    [InlineData("to apply", "Saved")]
    [InlineData("shortlisted", "Interested")]
    [InlineData("drafting", "Preparing")]
    [InlineData("in preparation", "Preparing")]
    public void Normalize_canonicalizes_prospect_synonyms(string input, string expected)
        => Assert.Equal(expected, JobPipeline.Normalize(input));

    [Theory]
    [InlineData("Saved")]
    [InlineData("Interested")]
    [InlineData("Preparing")]
    [InlineData("bookmarked")]
    public void IsProspect_true_for_pre_application_stages(string status)
        => Assert.True(JobPipeline.IsProspect(status));

    [Theory]
    [InlineData("Applied")]
    [InlineData("Waiting")]
    [InlineData("Interview")]
    [InlineData("Offer")]
    [InlineData("Rejected")]
    [InlineData("Ghosted")]
    public void IsProspect_false_for_post_application_stages(string status)
        => Assert.False(JobPipeline.IsProspect(status));

    [Fact]
    public void IsProspect_false_for_custom_status()
        // Custom statuses predate the split and have always counted as applied. Treating them as
        // prospects would silently drop them out of existing users' analytics.
        => Assert.False(JobPipeline.IsProspect("Take-home assignment"));

    [Fact]
    public void Prospect_stages_sort_before_applied()
    {
        Assert.True(JobPipeline.OrderOf("Saved") < JobPipeline.OrderOf("Interested"));
        Assert.True(JobPipeline.OrderOf("Interested") < JobPipeline.OrderOf("Preparing"));
        Assert.True(JobPipeline.OrderOf("Preparing") < JobPipeline.OrderOf("Applied"));
    }

    // --- SyncAppliedDate: DateApplied set <=> the job has left the Prospect stages -----------

    [Fact]
    public void SyncAppliedDate_clears_the_applied_date_for_a_prospect()
    {
        var now = new DateTime(2026, 7, 17, 12, 0, 0, DateTimeKind.Utc);
        var job = new JobApplication { Status = "Saved", DateApplied = now.AddDays(-5) };

        JobPipeline.SyncAppliedDate(job, now);

        Assert.Null(job.DateApplied);
    }

    [Fact]
    public void SyncAppliedDate_stamps_when_a_prospect_becomes_applied()
    {
        var now = new DateTime(2026, 7, 17, 12, 0, 0, DateTimeKind.Utc);
        var job = new JobApplication { Status = "Preparing", DateApplied = null };

        job.Status = "Applied";
        JobPipeline.SyncAppliedDate(job, now);

        Assert.Equal(now, job.DateApplied);
    }

    [Fact]
    public void SyncAppliedDate_preserves_an_existing_applied_date()
    {
        var now = new DateTime(2026, 7, 17, 12, 0, 0, DateTimeKind.Utc);
        var applied = now.AddDays(-30);
        var job = new JobApplication { Status = "Interview", DateApplied = applied };

        JobPipeline.SyncAppliedDate(job, now);

        Assert.Equal(applied, job.DateApplied);
    }

    [Fact]
    public void DaysSince_is_null_without_an_applied_date()
        => Assert.Null(new JobApplication { Status = "Saved", DateApplied = null }.DaysSince);

    [Fact]
    public void SyncAppliedDate_returns_the_cleared_date_so_callers_can_record_it()
    {
        var now = new DateTime(2026, 7, 17, 12, 0, 0, DateTimeKind.Utc);
        var applied = now.AddDays(-5);
        var job = new JobApplication { Status = "Saved", DateApplied = applied };

        var cleared = JobPipeline.SyncAppliedDate(job, now);

        // The controller persists this as an AppliedDateCleared JobEvent, so the application
        // activity survives a backwards move.
        Assert.Equal(applied, cleared);
        Assert.Null(job.DateApplied);
    }

    [Fact]
    public void SyncAppliedDate_returns_null_when_nothing_was_cleared()
    {
        var now = new DateTime(2026, 7, 17, 12, 0, 0, DateTimeKind.Utc);

        Assert.Null(JobPipeline.SyncAppliedDate(new JobApplication { Status = "Applied", DateApplied = now.AddDays(-1) }, now));
        Assert.Null(JobPipeline.SyncAppliedDate(new JobApplication { Status = "Saved", DateApplied = null }, now));
    }

    // --- Withdrawn + board grouping ---------------------------------------------------------

    [Theory]
    [InlineData("withdrew", "Withdrawn")]
    [InlineData("cancelled", "Withdrawn")]
    [InlineData("canceled", "Withdrawn")]
    public void Normalize_canonicalizes_withdrawn_synonyms(string input, string expected)
        => Assert.Equal(expected, JobPipeline.Normalize(input));

    [Fact]
    public void Declined_stays_rejected_and_does_not_become_withdrawn()
        // Opposite directions: the employer declined you vs you pulled out.
        => Assert.Equal("Rejected", JobPipeline.Normalize("declined"));

    [Fact]
    public void Withdrawn_is_closed_and_not_a_prospect()
    {
        Assert.False(JobPipeline.IsProspect("Withdrawn"));
        Assert.True(JobPipeline.IsCanonical("Withdrawn"));
    }

    [Fact]
    public void Ghosted_and_waiting_are_still_canonical_stages()
    {
        // Retained deliberately: the rules engine parks unanswered jobs in Ghosted, and Waiting
        // carries its own follow-up rule. Removing either would strand that behaviour.
        Assert.True(JobPipeline.IsCanonical("Ghosted"));
        Assert.True(JobPipeline.IsCanonical("Waiting"));
    }

    [Fact]
    public void Board_groups_match_the_agreed_layout()
    {
        Assert.Equal(new[] { "Saved", "Interested", "Preparing" }, JobPipeline.StagesInGroup(PipelineGroup.NotApplied));
        Assert.Equal(new[] { "Applied", "Waiting", "Interview", "Offer" }, JobPipeline.StagesInGroup(PipelineGroup.Active));
        Assert.Equal(new[] { "Rejected", "Ghosted", "Withdrawn" }, JobPipeline.StagesInGroup(PipelineGroup.Closed));
    }

    [Fact]
    public void Every_stage_belongs_to_exactly_one_group()
    {
        var grouped = JobPipeline.StagesInGroup(PipelineGroup.NotApplied)
            .Concat(JobPipeline.StagesInGroup(PipelineGroup.Active))
            .Concat(JobPipeline.StagesInGroup(PipelineGroup.Closed))
            .ToList();

        Assert.Equal(JobPipeline.Stages.Count, grouped.Count);
        Assert.Equal(grouped.Count, grouped.Distinct().Count());
    }

    [Fact]
    public void Offer_is_success_for_analytics_but_groups_under_active_for_the_board()
    {
        // The two axes deliberately disagree: StageAnalytics excludes Success from time-in-stage,
        // but the user is still actively working an Offer.
        var offer = JobPipeline.Stages.Single(s => s.Key == "Offer");
        Assert.Equal(PipelineCategory.Success, offer.Category);
        Assert.Equal(PipelineGroup.Active, offer.Group);
    }
}
