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

namespace JobTrackerApi.Tests;

// Phase 2.1-a — the diff engine. Pure comparison; applies nothing. These pin the conservative policy:
// clear matches only, updates only on non-empty differing values, no deletions, dedup, and the
// High/Medium/Low confidence markers that drive the review screen.
public sealed class CvProfileDiffServiceTests
{
    private static readonly CvProfileDiffService Svc = new();

    private static CvCategoryDiff Cat(CvImportDiff d, string name) => d.Categories.First(c => c.Category == name);

    private static StructuredCvJob Job(string title, string company, string? start = null, string? end = null, params string[] bullets)
        => new() { Title = title, Company = company, Start = start, End = end, Bullets = bullets.ToList() };

    [Fact]
    public void Empty_current_makes_everything_an_addition()
    {
        var current = new StructuredCvProfile();
        var extracted = new StructuredCvProfile
        {
            Jobs = { Job("System Developer", "Warwickshire County Council", "2015", "2023", "Built full-stack apps.") },
            Projects = { new StructuredCvProject { Name = "JobTrack", Bullets = { "Job tracker." } } },
            Skills = { "C#", ".NET", "Docker" },
            Languages = { new StructuredCvLanguage { Name = "English", Level = "Native" }, new StructuredCvLanguage { Name = "Norwegian", Level = "B1" } },
        };

        var diff = Svc.Diff(current, extracted);

        Assert.True(diff.HasChanges);
        Assert.Single(Cat(diff, "Experience").Added);
        Assert.Single(Cat(diff, "Projects").Added);
        Assert.Equal(3, Cat(diff, "Skills").Added.Count);
        Assert.Equal(2, Cat(diff, "Languages").Added.Count);
        Assert.Empty(Cat(diff, "Experience").Updated);
    }

    [Fact]
    public void A_matched_job_with_no_new_information_is_unchanged_not_duplicated()
    {
        var job = Job("System Developer", "Warwickshire County Council", "2015", "2023", "Built apps.");
        var current = new StructuredCvProfile { Jobs = { job } };
        var extracted = new StructuredCvProfile { Jobs = { Job("system developer", "WARWICKSHIRE COUNTY COUNCIL", "2015", "2023", "Built apps.") } };

        var exp = Cat(Svc.Diff(current, extracted), "Experience");

        Assert.Empty(exp.Added);      // same company+title -> not a new entry
        Assert.Empty(exp.Updated);    // same dates + bullets -> nothing to update
        Assert.Equal(1, exp.UnchangedCount);
    }

    [Fact]
    public void A_matched_job_with_new_dates_is_an_update_not_an_add()
    {
        var current = new StructuredCvProfile { Jobs = { Job("System Developer", "Warwickshire County Council") } };
        var extracted = new StructuredCvProfile { Jobs = { Job("System Developer", "Warwickshire County Council", "2015", "2023") } };

        var exp = Cat(Svc.Diff(current, extracted), "Experience");

        Assert.Empty(exp.Added);
        Assert.Single(exp.Updated);
        Assert.Contains(exp.Updated[0].FieldChanges, f => f.Field == "Dates");
    }

    [Fact]
    public void A_genuinely_new_job_is_an_addition()
    {
        var current = new StructuredCvProfile { Jobs = { Job("System Developer", "Warwickshire County Council") } };
        var extracted = new StructuredCvProfile
        {
            Jobs = { Job("System Developer", "Warwickshire County Council"), Job("Bartender", "The Hodcarrier", "2016", "2018") },
        };

        var exp = Cat(Svc.Diff(current, extracted), "Experience");

        Assert.Single(exp.Added);
        Assert.Equal("Bartender — The Hodcarrier", exp.Added[0].Label);
    }

    [Fact]
    public void Skills_dedup_case_insensitively_and_only_new_ones_are_added()
    {
        var current = new StructuredCvProfile { Skills = { "C#", "SQL" } };
        var extracted = new StructuredCvProfile { Skills = { "c#", ".NET", "sql", "Docker", "docker" } };

        var skills = Cat(Svc.Diff(current, extracted), "Skills");

        Assert.Equal(2, skills.Added.Count); // .NET and Docker only, deduped
        Assert.Contains(skills.Added, s => s.Label == ".NET");
        Assert.Contains(skills.Added, s => s.Label == "Docker");
    }

    [Fact]
    public void A_language_level_change_is_an_update_a_new_language_is_an_add()
    {
        var current = new StructuredCvProfile { Languages = { new StructuredCvLanguage { Name = "English", Level = "Native" } } };
        var extracted = new StructuredCvProfile
        {
            Languages =
            {
                new StructuredCvLanguage { Name = "English", Level = "Native" },
                new StructuredCvLanguage { Name = "Norwegian", Level = "B1" },
            },
        };

        var langs = Cat(Svc.Diff(current, extracted), "Languages");
        Assert.Single(langs.Added);
        Assert.Equal("Norwegian (B1)", langs.Added[0].Label);
        Assert.Equal(1, langs.UnchangedCount);
    }

    [Fact]
    public void A_language_without_a_level_is_low_confidence()
    {
        var extracted = new StructuredCvProfile { Languages = { new StructuredCvLanguage { Name = "French" } } };
        var langs = Cat(Svc.Diff(new StructuredCvProfile(), extracted), "Languages");
        Assert.Equal("Low", langs.Added[0].Confidence);
        Assert.Equal(1, diffLow(langs));
    }

    private static int diffLow(CvCategoryDiff c) => c.LowConfidenceCount;

    [Fact]
    public void Extraction_never_proposes_blanking_an_existing_value_and_never_deletes()
    {
        // current has a rich job; extraction found the same job but with an empty location and no bullets.
        var current = new StructuredCvProfile { Jobs = { Job("Dev", "Acme", "2019", "2022", "Did things.") } };
        current.Jobs[0].Location = "Oslo";
        var extracted = new StructuredCvProfile { Jobs = { Job("Dev", "Acme", "2019", "2022") } }; // no location, no bullets

        var exp = Cat(Svc.Diff(current, extracted), "Experience");

        Assert.Empty(exp.Added);
        Assert.Empty(exp.Updated);        // empty extracted fields never overwrite
        Assert.Equal(1, exp.UnchangedCount);

        // And a job the extraction did NOT mention simply doesn't appear in the diff (never deleted).
        var extracted2 = new StructuredCvProfile();
        Assert.False(Svc.Diff(current, extracted2).HasChanges);
    }
    [Fact]
    public void Merge_preserves_existing_items_and_adds_new_information()
    {
        var current = new StructuredCvProfile
        {
            Jobs = { Job("Developer", "Acme", "2020", "2022", "Curated bullet") },
            Skills = { "C#" },
        };
        current.Jobs[0].Id = "keep-me";
        current.Jobs[0].Location = "Oslo, Norway and Remote across Europe";
        current.Contact.Website = "https://example.test/portfolio/ada?view=full";
        var extracted = new StructuredCvProfile
        {
            Jobs =
            {
                Job("developer", "ACME", "2020", "2024", "Curated bullet", "New extracted bullet"),
                Job("Engineer", "New Co", "2024", "Present", "Built systems"),
            },
            Skills = { "c#", "Docker" },
        };

        var merged = Svc.Merge(current, extracted);

        Assert.Equal(2, merged.Jobs.Count);
        Assert.Equal("keep-me", merged.Jobs[0].Id);
        Assert.Equal("Oslo, Norway and Remote across Europe", merged.Jobs[0].Location);
        Assert.Equal("https://example.test/portfolio/ada?view=full", merged.Contact.Website);
        Assert.Equal("2024", merged.Jobs[0].End);
        Assert.Equal(new[] { "Curated bullet", "New extracted bullet" }, merged.Jobs[0].Bullets);
        Assert.Equal(new[] { "C#", "Docker" }, merged.Skills);
    }

    [Fact]
    public void Merge_never_blanks_or_deletes_existing_data()
    {
        var current = new StructuredCvProfile
        {
            Contact = new StructuredCvContact { FullName = "Demo User", Email = "demo@example.com" },
            Jobs = { Job("Developer", "Acme", "2020", "2024", "Keep this") },
            Languages = { new StructuredCvLanguage { Name = "English", Level = "Native" } },
        };
        var extracted = new StructuredCvProfile
        {
            Contact = new StructuredCvContact { FullName = "Demo User" },
            Languages = { new StructuredCvLanguage { Name = "English" } },
        };

        var merged = Svc.Merge(current, extracted);

        Assert.Equal("demo@example.com", merged.Contact.Email);
        Assert.Single(merged.Jobs);
        Assert.Equal("Native", merged.Languages[0].Level);
    }

    [Fact]
    public void Merge_requires_explicit_acceptance_for_each_low_confidence_change()
    {
        var extracted = new StructuredCvProfile
        {
            Languages =
            {
                new StructuredCvLanguage { Name = "French" },
                new StructuredCvLanguage { Name = "Norwegian", Level = "B1" },
            },
        };
        var diff = Svc.Diff(new StructuredCvProfile(), extracted);
        var french = Cat(diff, "Languages").Added.Single(x => x.Label == "French");

        Assert.Equal("Languages|french", french.Id);
        var defaultMerge = Svc.Merge(new StructuredCvProfile(), extracted);
        var confirmedMerge = Svc.Merge(new StructuredCvProfile(), extracted, new HashSet<string> { french.Id });

        Assert.DoesNotContain(defaultMerge.Languages, x => x.Name == "French");
        Assert.Contains(defaultMerge.Languages, x => x.Name == "Norwegian");
        Assert.Contains(confirmedMerge.Languages, x => x.Name == "French");
    }

}
