using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Tests.TestSupport;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class JobApplicationsApplicationPackageTests
{
    [Fact]
    public async Task Save_application_drafts_replaces_notes_instead_of_appending()
    {
        await using var db = CreateDb();
        var company = new Company
        {
            Name = "Acme",
            OwnerUserId = "user-1"
        };
        db.Companies.Add(company);
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            Notes = "Old notes"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
        var result = await controller.SaveApplicationDrafts(job.Id, new SaveApplicationDraftsRequest(null, "Updated notes block", null), CancellationToken.None);

        Assert.IsType<NoContentResult>(result);
        var saved = await db.JobApplications.FirstAsync();
        Assert.Equal("Updated notes block", saved.Notes);
    }

    [Fact]
    public async Task Save_application_drafts_updates_and_clears_answer_without_changing_human_notes()
    {
        await using var db = CreateDb();
        var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
        db.Companies.Add(company);
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            Notes = "Human note\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nOld answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>",
            RecruiterMessageDraft = "Old recruiter message"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
        var updated = await controller.SaveApplicationDrafts(
            job.Id,
            new SaveApplicationDraftsRequest(null, null, " New recruiter message ", " New answer "),
            CancellationToken.None);

        Assert.IsType<NoContentResult>(updated);
        Assert.Equal("Human note", JobApplicationHelpers.RemoveSavedApplicationAnswerDraft(job.Notes));
        Assert.Equal("New answer", JobApplicationHelpers.ExtractSavedApplicationAnswerDraft(job.Notes));
        Assert.Equal("New recruiter message", job.RecruiterMessageDraft);

        job.Notes += "\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nDuplicate stale answer\n<<<END_APPLICATION_ANSWER_DRAFT>>>";
        var cleared = await controller.SaveApplicationDrafts(
            job.Id,
            new SaveApplicationDraftsRequest(null, null, "", ""),
            CancellationToken.None);

        Assert.IsType<NoContentResult>(cleared);
        Assert.Equal("Human note", job.Notes);
        Assert.Null(job.RecruiterMessageDraft);
    }

    [Fact]
    public async Task Save_application_drafts_cannot_mutate_another_users_application()
    {
        await using var db = CreateDb();
        var company = new Company { Name = "Other Acme", OwnerUserId = "user-2" };
        db.Companies.Add(company);
        await db.SaveChangesAsync();

        var otherUsersJob = new JobApplication
        {
            JobTitle = "Private role",
            CompanyId = company.Id,
            OwnerUserId = "user-2",
            Notes = "Private note",
            RecruiterMessageDraft = "Private draft"
        };
        db.JobApplications.Add(otherUsersJob);
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
        var result = await controller.SaveApplicationDrafts(
            otherUsersJob.Id,
            new SaveApplicationDraftsRequest(null, null, "Changed", "Changed"),
            CancellationToken.None);

        Assert.IsType<NotFoundResult>(result);
        Assert.Equal("Private note", otherUsersJob.Notes);
        Assert.Equal("Private draft", otherUsersJob.RecruiterMessageDraft);
    }

    [Fact]
    public async Task Generate_application_package_uses_imported_correspondence_and_recruiter_context()
    {
        await using var db = CreateDb();
        var company = new Company
        {
            Name = "Acme",
            RecruiterName = "Maria Recruiter",
            RecruiterEmail = "maria@acme.test",
            OwnerUserId = "user-1"
        };
        db.Companies.Add(company);
        db.Users.Add(new ApplicationUser
        {
            Id = "user-1",
            UserName = "user@example.test",
            Email = "user@example.test",
            ProfileCvText = "Built .NET APIs and led backend delivery.",
            ProfileCvStructureJson = "[]"
        });
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            Description = "Need .NET, APIs, and async collaboration with recruiters.",
            Notes = "Priority role",
            ShortSummary = "Acme backend hiring"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = job.Id,
            From = "Company",
            Subject = "Backend Developer interview",
            ExternalThreadId = "thread-1",
            ExternalFrom = "Maria Recruiter <maria@acme.test>",
            ExternalTo = "user@example.test",
            Content = "We want someone who can own .NET APIs and communicate clearly with stakeholders.",
            Date = DateTime.UtcNow.AddDays(-1)
        });
        await db.SaveChangesAsync();

        var summarizer = new Mock<ISummarizerService>();
        summarizer
            .Setup(service => service.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync((string instruction, string context, int _, int __) =>
            {
                if (instruction.Contains("List up to 4 concrete application-package signals", StringComparison.OrdinalIgnoreCase))
                {
                    return "Use recruiter language about owning .NET APIs.\nMention the interview timeline from imported correspondence.";
                }

                if (instruction.Contains("cover letter", StringComparison.OrdinalIgnoreCase))
                {
                    return context.Contains("Imported correspondence context:", StringComparison.OrdinalIgnoreCase)
                        && context.Contains("Maria Recruiter", StringComparison.OrdinalIgnoreCase)
                        ? "Cover letter tailored with recruiter context and imported correspondence."
                        : "Generic cover letter.";
                }

                if (instruction.Contains("recruiter intro message", StringComparison.OrdinalIgnoreCase))
                {
                    return context.Contains("Recruiter email: maria@acme.test", StringComparison.OrdinalIgnoreCase)
                        ? "Recruiter message that uses Maria's context."
                        : "Generic recruiter message.";
                }

                if (instruction.Contains("application answer", StringComparison.OrdinalIgnoreCase))
                {
                    return "Application answer grounded in the imported thread.";
                }

                if (instruction.Contains("Rewrite the candidate CV", StringComparison.OrdinalIgnoreCase))
                {
                    return "Tailored CV that highlights .NET API ownership for Acme.";
                }

                return "Variant draft";
            });

        var controller = CreateController(db, summarizer.Object, "user-1");
        var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GenerateApplicationPackageDto>(ok.Value);

        Assert.Contains("Tailored CV", payload.TailoredCvText);
        Assert.Equal("Cover letter tailored with recruiter context and imported correspondence.", payload.CoverLetterDraft);
        Assert.Equal("Recruiter message that uses Maria's context.", payload.RecruiterMessageDraft);
        Assert.Contains(payload.KeyPoints, item => item.Contains("interview timeline", StringComparison.OrdinalIgnoreCase));
        Assert.Contains(payload.KeyPoints, item => item.Contains("recruiter language", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public async Task Generate_application_package_passes_typed_structured_cv_context_to_summarizer()
    {
        await using var db = CreateDb();
        var company = new Company
        {
            Name = "Acme",
            OwnerUserId = "user-1"
        };
        db.Companies.Add(company);
        db.Users.Add(new ApplicationUser
        {
            Id = "user-1",
            UserName = "user@example.test",
            Email = "user@example.test",
            ProfileCvText = "Built APIs and shipped backend work.",
            ProfileCvStructureJson = """
            {
              "version": "1",
              "contact": {
                "fullName": "Demo User",
                "headline": "Backend Developer",
                "email": "user@example.test",
                "location": "Oslo"
              },
              "summary": ["Backend-focused developer with strong API delivery experience."],
              "jobs": [
                {
                  "title": "System Developer",
                  "company": "Acme Consulting",
                  "location": "Oslo",
                  "start": "2020",
                  "end": "2024",
                  "isCurrent": false,
                  "bullets": ["Owned .NET API delivery across multiple services."],
                  "skills": [".NET", "SQL", "APIs"]
                }
              ],
              "education": [],
              "skills": [".NET", "SQL", "APIs"],
              "languages": [{ "name": "English", "level": "Native" }],
              "interests": [],
              "otherSections": []
            }
            """
        });
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            Description = "Need .NET API ownership and strong SQL skills."
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        string? capturedContext = null;
        var summarizer = new Mock<ISummarizerService>();
        summarizer
            .Setup(service => service.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync((string instruction, string context, int _, int __) =>
            {
                if (instruction.Contains("Rewrite the candidate CV", StringComparison.OrdinalIgnoreCase))
                {
                    capturedContext = context;
                    return "Tailored CV";
                }

                if (instruction.Contains("List up to 4 concrete application-package signals", StringComparison.OrdinalIgnoreCase))
                {
                    return "Lead with .NET API ownership.";
                }

                return "Draft";
            });

        var controller = CreateController(db, summarizer.Object, "user-1");
        var result = await controller.GenerateApplicationPackage(job.Id, null, null, null, CancellationToken.None);

        Assert.IsType<OkObjectResult>(result.Result);
        Assert.NotNull(capturedContext);
        Assert.Contains("Structured CV:", capturedContext);
        Assert.Contains("Name: Demo User", capturedContext);
        Assert.Contains("Skills:\n.NET, SQL, APIs", capturedContext);
        Assert.Contains("Work Experience:", capturedContext);
        Assert.Contains("Owned .NET API delivery across multiple services.", capturedContext);
    }

    [Fact]
    public async Task Get_tailored_cv_draft_returns_legacy_fallback_when_no_structured_draft_exists()
    {
        await using var db = CreateDb();
        var company = new Company
        {
            Name = "Acme",
            OwnerUserId = "user-1"
        };
        db.Companies.Add(company);
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            TailoredCvText = "Existing tailored CV text",
            TailoredCvUpdatedAt = DateTime.UtcNow
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1");
        var result = await controller.GetTailoredCvDraft(job.Id, CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<TailoredCvDraftDto>(ok.Value);
        Assert.True(payload.IsLegacyFallback);
        Assert.Equal("legacy-text", payload.TemplateId);
        Assert.Contains("Existing tailored CV text", payload.RenderedText);
    }

    [Fact]
    public async Task Generate_and_save_tailored_cv_draft_persists_job_scoped_document_without_mutating_master_cv()
    {
        await using var db = CreateDb();
        var company = new Company
        {
            Name = "Acme",
            OwnerUserId = "user-1"
        };
        db.Companies.Add(company);
        db.Users.Add(new ApplicationUser
        {
            Id = "user-1",
            UserName = "user@example.test",
            Email = "user@example.test",
            CurrentCvProfileVersion = 7,
            ProfileCvText = "Built APIs and owned backend delivery.",
            ProfileCvStructureJson = """
            {
              "version": "1",
              "contact": {
                "fullName": "Demo User",
                "headline": "Backend Developer"
              },
              "summary": ["Backend-focused developer with API delivery experience."],
              "jobs": [
                {
                  "title": "System Developer",
                  "company": "Acme Consulting",
                  "location": "Oslo",
                  "start": "2021",
                  "end": "2024",
                  "isCurrent": false,
                  "bullets": ["Owned .NET API delivery across multiple services."],
                  "skills": [".NET", "SQL", "APIs"]
                }
              ],
              "education": [],
              "skills": [".NET", "SQL", "APIs"],
              "languages": [{ "name": "English", "level": "Native" }],
              "interests": [],
              "otherSections": []
            }
            """
        });
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            Description = "Need .NET API ownership and strong SQL skills."
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var summarizer = new Mock<ISummarizerService>();
        summarizer
            .Setup(service => service.SummarizeSectionAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync((string instruction, string _, int __, int ___) =>
            {
                if (instruction.Contains("headline", StringComparison.OrdinalIgnoreCase)) return "Senior Backend Engineer";
                if (instruction.Contains("summary bullets", StringComparison.OrdinalIgnoreCase)) return "Led backend API delivery.\nImproved SQL-backed workflows.";
                return "Draft";
            });

        var controller = CreateController(db, summarizer.Object, "user-1");
        var generateResult = await controller.GenerateTailoredCvDraft(job.Id, "ats", CancellationToken.None);
        var generateOk = Assert.IsType<OkObjectResult>(generateResult.Result);
        var generated = Assert.IsType<TailoredCvDraftDto>(generateOk.Value);

        Assert.False(generated.IsLegacyFallback);
        Assert.Equal(7, generated.CanonicalProfileVersion);
        Assert.Equal("Senior Backend Engineer", generated.Headline);
        Assert.Contains("Led backend API delivery.", generated.RenderedText);

        var saveResult = await controller.SaveTailoredCvDraft(job.Id, new SaveTailoredCvDraftRequest(
            generated.TemplateId,
            "Principal Backend Engineer",
            new List<string> { "Own backend delivery for critical APIs." },
            new List<string> { ".NET", "SQL" },
            generated.Experience,
            generated.Education,
            generated.CustomSections,
            generated.RenderOptions,
            "edited"), CancellationToken.None);

        Assert.IsType<NoContentResult>(saveResult);

        var savedDraft = await db.TailoredCvDrafts.SingleAsync();
        var savedJob = await db.JobApplications.SingleAsync();
        var savedUser = await db.Users.SingleAsync();

        Assert.Equal("edited", savedDraft.Status);
        Assert.Equal(7, savedDraft.CanonicalProfileVersion);
        Assert.Contains("Principal Backend Engineer", savedJob.TailoredCvText);
        Assert.Equal("Built APIs and owned backend delivery.", savedUser.ProfileCvText);
    }

    [Fact]
    public async Task Preview_and_export_tailored_cv_use_same_renderer_contract_and_profile_avatar_default()
    {
        await using var db = CreateDb();
        var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
        db.Companies.Add(company);
        db.Users.Add(new ApplicationUser
        {
            Id = "user-1",
            UserName = "user@example.test",
            Email = "user@example.test",
            AvatarImageDataUrl = "data:image/png;base64,abc123"
        });
        await db.SaveChangesAsync();

        var job = new JobApplication
        {
            JobTitle = "Backend Developer",
            CompanyId = company.Id,
            OwnerUserId = "user-1",
            TailoredCvText = "Saved tailored CV"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var renderer = new TestCvTemplateRenderer();
        var exporter = new TestCvPdfExporter();
        var controller = CreateController(db, Mock.Of<ISummarizerService>(), "user-1", renderer, exporter);
        var request = new TailoredCvRenderRequest(
            "ats-minimal",
            "Backend Engineer",
            new List<string> { "Built APIs" },
            new List<string> { ".NET" },
            new List<TailoredCvExperienceItem>(),
            new List<TailoredCvEducationItem>(),
            new List<TailoredCvCustomSection>(),
            new TailoredCvRenderOptions { ShowPhoto = true, AccentColor = "#123456" },
            null,
            true);

        var previewResult = await controller.PreviewTailoredCv(job.Id, request, CancellationToken.None);
        var ok = Assert.IsType<OkObjectResult>(previewResult.Result);
        var preview = Assert.IsType<TailoredCvPreviewDto>(ok.Value);
        Assert.Equal("ats-minimal", preview.TemplateId);
        Assert.Equal("preview.pdf", preview.SuggestedFileName);
        Assert.Equal("data:image/png;base64,abc123", renderer.LastPhotoDataUrl);

        var exportResult = await controller.ExportTailoredCvPdf(job.Id, request, CancellationToken.None);
        var file = Assert.IsType<FileContentResult>(exportResult);
        Assert.Equal("application/pdf", file.ContentType);
        Assert.Equal("preview.pdf", file.FileDownloadName);
        Assert.NotNull(exporter.LastRenderResult);
        Assert.Equal(preview.Html, exporter.LastRenderResult!.Html);
    }

    [Fact]
    public void Template_renderer_supports_named_variants()
    {
        var renderer = new CvTemplateRenderer();
        var document = TailoredCvDraftJson.Normalize(new TailoredCvDocument
        {
            TemplateId = "harvard",
            Headline = "Product Manager",
            Summary = new List<string> { "Built and shipped product roadmaps." },
            SelectedSkills = new List<string> { "Strategy", "Stakeholder management" },
            Experience = new List<TailoredCvExperienceItem>
            {
                new() { Title = "Product Manager", Company = "Acme", Start = "2022", End = "2025", Bullets = new List<string> { "Launched new product line." } }
            }
        });

        var harvard = renderer.Render(document, "harvard", "Andrew O'Sullivan", "Product Manager", "Acme", null);
        var auckland = renderer.Render(document, "auckland", "Andrew O'Sullivan", "Product Manager", "Acme", "data:image/png;base64,abc");
        var edinburgh = renderer.Render(document, "edinburgh", "Andrew O'Sullivan", "Product Manager", "Acme", "data:image/png;base64,abc");

        Assert.Equal("harvard", harvard.TemplateId);
        Assert.Contains("Template: ATS Minimal", renderer.Render(document, "ats-minimal", "Andrew O'Sullivan", "Product Manager", "Acme", null).Html);
        Assert.Contains("Andrew O&#39;Sullivan", harvard.Html);
        Assert.Contains("sidebar", auckland.Html, StringComparison.OrdinalIgnoreCase);
        Assert.Contains("curved", edinburgh.Html, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public void Tailored_template_catalog_preserves_legacy_ids_and_defaults()
    {
        Assert.Equal(
            new[] { "ats-minimal", "harvard", "auckland", "edinburgh", "monarch", "fjord" },
            TailoredCvTemplateCatalog.Templates.Select(template => template.Id));
        Assert.Equal("ats-minimal", TailoredCvTemplateCatalog.NormalizeId("legacy-text"));
        Assert.Equal("ats-minimal", TailoredCvTemplateCatalog.NormalizeId("unknown"));
        Assert.Equal("brick", TailoredCvTemplateCatalog.Resolve("harvard").AccentColor);
        Assert.Contains("Harvard template", TailoredCvTemplateCatalog.Resolve("harvard").RewriteGuidance);
        Assert.Equal("#047857", TailoredCvTemplateCatalog.ResolveAccent("emerald"));
        Assert.Equal("#123456", TailoredCvTemplateCatalog.ResolveAccent("#123456"));
    }

    [Fact]
    public void Template_renderer_wraps_long_content_and_escapes_sidebar_values()
    {
        var document = TailoredCvDraftJson.Normalize(new TailoredCvDocument
        {
            Headline = new string('H', 180),
            SelectedSkills = new List<string> { $"<script>alert(1)</script>{new string('s', 400)}" },
            Experience = new List<TailoredCvExperienceItem>
            {
                new()
                {
                    Title = new string('T', 220),
                    Company = new string('C', 220),
                    Start = "2020",
                    End = "Present",
                    Bullets = Enumerable.Range(1, 7).Select(index => index == 1 ? new string('x', 500) : $"Achievement {index}").ToList(),
                },
            },
        });

        var html = new CvTemplateRenderer().Render(document, "auckland", new string('N', 180), "Engineer", "Acme", null).Html;

        Assert.Contains("class=\"entry entry-flow\"", html);
        Assert.Contains("class=\"item-flow\"", html);
        Assert.Contains("grid-template-columns:34% minmax(0,66%)", html);
        Assert.Contains("overflow-wrap:anywhere", html);
        Assert.Contains("&lt;script&gt;alert(1)&lt;/script&gt;", html);
        Assert.DoesNotContain("<script>", html);
    }

    private static JobApplicationsController CreateController(JobTrackerContext db, ISummarizerService summarizer, string userId, ICvTemplateRenderer? renderer = null, ICvPdfExporter? exporter = null)
    {
        var user = db.Users.AsNoTracking().FirstOrDefault(x => x.Id == userId);
        var controller = new JobApplicationsController(
            db,
            summarizer,
            CreateUserManager(user).Object,
            renderer ?? new TestCvTemplateRenderer(),
            exporter ?? new TestCvPdfExporter());
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(new ClaimsIdentity(new[]
                {
                    new Claim(ClaimTypes.NameIdentifier, userId)
                }, "test"))
            }
        };
        return controller;
    }

    private static JobTrackerContext CreateDb()
    {
        return TestHostFactory.CreateInMemoryDb();
    }

    private static Mock<UserManager<ApplicationUser>> CreateUserManager(ApplicationUser? user = null)
    {
        return TestHostFactory.CreateUserManager(user);
    }

    private sealed class TestCvTemplateRenderer : ICvTemplateRenderer
    {
        public string? LastPhotoDataUrl { get; private set; }

        public TailoredCvRenderResult Render(TailoredCvDocument? document, string? templateId, string candidateName, string jobTitle, string? companyName, string? photoDataUrl = null)
        {
            LastPhotoDataUrl = photoDataUrl;
            return new TailoredCvRenderResult(templateId ?? "ats-minimal", "preview.pdf", $"<html><body>{candidateName}|{jobTitle}|{companyName}|{document?.Headline}|{photoDataUrl}</body></html>");
        }
    }

    private sealed class TestCvPdfExporter : ICvPdfExporter
    {
        public TailoredCvRenderResult? LastRenderResult { get; private set; }

        public Task<CvPdfArtifact> ExportAsync(string ownerUserId, TailoredCvRenderResult renderResult, CancellationToken cancellationToken)
        {
            LastRenderResult = renderResult;
            return Task.FromResult(new CvPdfArtifact("preview.pdf", "/tmp/preview.pdf", new byte[] { 1, 2, 3 }));
        }
    }
}
