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.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class GmailControllerTests
{
    [Fact]
    public async Task Status_returns_sync_state_fields_for_connected_account()
    {
        await using var db = CreateDb();
        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection
            {
                OwnerUserId = "user-1",
                GmailAddress = "user@example.test",
                ConnectedAt = DateTimeOffset.UtcNow.AddDays(-3),
                LastSyncedAt = DateTimeOffset.UtcNow.AddMinutes(-10),
                LastSyncAttemptedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
                LastSyncSucceededAt = DateTimeOffset.UtcNow.AddMinutes(-10),
                LastSyncMode = "list-messages",
                LastSyncSource = "custom-query",
                LastSyncStatus = "error",
                LastSyncError = "Token refresh failed"
            });

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.Status(CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailConnectionStatusDto>(ok.Value);
        Assert.True(payload.Connected);
        Assert.Equal("user@example.test", payload.GmailAddress);
        Assert.Equal("list-messages", payload.LastSyncMode);
        Assert.Equal("custom-query", payload.LastSyncSource);
        Assert.Equal("error", payload.LastSyncStatus);
        Assert.Equal("Token refresh failed", payload.LastSyncError);
    }

    [Fact]
    public async Task Import_thread_rejects_missing_message_ids()
    {
        await using var db = CreateDb();
        var controller = CreateController(db, Mock.Of<IGmailOAuthService>(), "user-1");

        var result = await controller.ImportThread(new ImportGmailThreadRequest(1, "thread-1", Array.Empty<string>()), CancellationToken.None);

        var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
        Assert.Equal("At least one messageId is required.", badRequest.Value);
    }

    [Fact]
    public async Task Job_candidates_rejects_invalid_job_id()
    {
        await using var db = CreateDb();
        var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
        var controller = CreateController(db, gmail.Object, "user-1");

        var result = await controller.JobCandidates(0, null, 6, CancellationToken.None);

        var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
        Assert.Equal("Valid jobApplicationId is required.", badRequest.Value);
        gmail.Verify(service => service.ListJobCandidateMessagesAsync(It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Fact]
    public async Task Job_candidates_returns_ranked_threads_with_match_reasons_query_hits_and_import_flags()
    {
        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);
        await db.SaveChangesAsync();

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

        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = job.Id,
            From = "Company",
            Subject = "Backend Developer interview",
            ExternalMessageId = "msg-imported",
            ExternalThreadId = "thread-top",
            Content = "Earlier imported thread"
        });
        await db.SaveChangesAsync();

        var overrideQuery = "label:important";
        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailQueryMatchedMessage(
                    new GmailMessageSummary(
                        "msg-top",
                        "thread-top",
                        "Backend Developer interview",
                        "Maria Recruiter <maria@acme.test>",
                        "user@example.test",
                        DateTimeOffset.UtcNow.AddDays(-2),
                        "Acme wants to schedule a backend developer interview."),
                    new[]
                    {
                        overrideQuery,
                        "\"Acme\" \"Backend Developer\" newer_than:365d"
                    }),
                new GmailQueryMatchedMessage(
                    new GmailMessageSummary(
                        "msg-imported",
                        "thread-top",
                        "Backend Developer interview",
                        "Maria Recruiter <maria@acme.test>",
                        "user@example.test",
                        DateTimeOffset.UtcNow.AddDays(-4),
                        "Already imported message."),
                    new[]
                    {
                        "(from:maria@acme.test OR to:maria@acme.test) newer_than:365d"
                    }),
                new GmailQueryMatchedMessage(
                    new GmailMessageSummary(
                        "msg-low",
                        "thread-low",
                        "Backend update",
                        "alerts@example.test",
                        "user@example.test",
                        DateTimeOffset.UtcNow.AddDays(-90),
                        "A generic backend role update without recruiter context."),
                    new[]
                    {
                        "subject:\"Backend Developer\" newer_than:365d"
                    })
            });

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.JobCandidates(job.Id, overrideQuery, 6, CancellationToken.None);

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

        Assert.Equal(job.Id, payload.JobApplicationId);
        Assert.Contains(overrideQuery, payload.Queries);
        Assert.Equal(3, payload.CandidateMessageCount);
        Assert.Equal(2, payload.CandidateThreadCount);
        Assert.Equal(2, payload.Threads.Count);

        var topThread = payload.Threads[0];
        Assert.Equal("thread-top", topThread.ThreadId);
        Assert.Equal("high", topThread.Confidence);
        Assert.True(topThread.HasImportedMessages);
        Assert.Equal(1, topThread.ImportedMessageCount);
        Assert.Equal(2, topThread.MessageCount);
        Assert.Contains(overrideQuery, topThread.MatchedQueries);
        Assert.Contains(topThread.MatchReasons, reason => reason.Label == "company" && reason.Value == "Acme" && reason.Points == 18);
        Assert.Contains(topThread.MatchReasons, reason => reason.Label == "recruiterEmail" && reason.Value == "maria@acme.test" && reason.Points == 20);
        Assert.Contains(topThread.MatchReasons, reason => reason.Label == "queryHits" && reason.Value == "2" && reason.Points == 8);

        var importedMessage = Assert.Single(topThread.Messages, message => message.Id == "msg-imported");
        Assert.True(importedMessage.AlreadyImported);
        Assert.Contains(importedMessage.MatchReasons, reason => reason.Label == "status" && reason.Value == "already-imported" && reason.Points == 0);

        var freshMessage = Assert.Single(topThread.Messages, message => message.Id == "msg-top");
        Assert.False(freshMessage.AlreadyImported);
        Assert.Contains(overrideQuery, freshMessage.MatchedQueries);
        Assert.Contains(freshMessage.MatchReasons, reason => reason.Label == "status" && reason.Value == "thread-already-imported" && reason.Points == 0);

        var lowThread = payload.Threads[1];
        Assert.Equal("thread-low", lowThread.ThreadId);
        Assert.Equal("low", lowThread.Confidence);
    }

    [Fact]
    public async Task Job_candidates_returns_empty_threads_when_gmail_has_no_matches()
    {
        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"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
            .ReturnsAsync(Array.Empty<GmailQueryMatchedMessage>());

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

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailJobMatchesResponseDto>(ok.Value);
        Assert.NotEmpty(payload.Queries);
        Assert.Equal(0, payload.CandidateMessageCount);
        Assert.Equal(0, payload.CandidateThreadCount);
        Assert.Empty(payload.Threads);
    }

    [Fact]
    public async Task Import_returns_message_metadata_and_skips_repeat_message_imports()
    {
        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"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-1",
                "thread-1",
                "Interview update",
                "Maria Recruiter <maria@acme.test>",
                "user@example.test",
                DateTimeOffset.UtcNow.AddDays(-1),
                "Snippet",
                "Body text",
                null,
                new[] { "INBOX", "IMPORTANT" },
                new[] { new GmailMessageAttachment("cv.pdf", "application/pdf", 2048, "att-1", false) }));

        var controller = CreateController(db, gmail.Object, "user-1");

        var first = await controller.Import(new ImportGmailMessageRequest(job.Id, "msg-1"), CancellationToken.None);
        var firstOk = Assert.IsType<OkObjectResult>(first.Result);
        var firstPayload = Assert.IsType<GmailImportMessageResultDto>(firstOk.Value);
        Assert.Equal(1, firstPayload.Imported);
        Assert.Equal(0, firstPayload.Skipped);
        Assert.Equal("thread-1", firstPayload.ThreadId);
        Assert.NotNull(firstPayload.Message);
        Assert.Equal("thread-1", firstPayload.Message!.ExternalThreadId);
        Assert.Equal("Maria Recruiter <maria@acme.test>", firstPayload.Message.ExternalFrom);
        Assert.Equal("user@example.test", firstPayload.Message.ExternalTo);
        Assert.Equal("inbound", firstPayload.Message.Direction);
        Assert.Contains("IMPORTANT", firstPayload.Message.ExternalLabels);
        Assert.Single(firstPayload.Message.AttachmentMetadata);
        Assert.Equal("cv.pdf", firstPayload.Message.AttachmentMetadata[0].FileName);

        var second = await controller.Import(new ImportGmailMessageRequest(job.Id, "msg-1"), CancellationToken.None);
        var secondOk = Assert.IsType<OkObjectResult>(second.Result);
        var secondPayload = Assert.IsType<GmailImportMessageResultDto>(secondOk.Value);
        Assert.Equal(0, secondPayload.Imported);
        Assert.Equal(1, secondPayload.Skipped);
        Assert.Equal("thread-1", secondPayload.ThreadId);

        var storedMessages = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).ToListAsync();
        Assert.Single(storedMessages);
        Assert.Equal("gmail", storedMessages[0].Provider);
        gmail.Verify(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()), Times.Once);
    }

    [Fact]
    public async Task Import_thread_reports_imported_and_skipped_counts_for_repeat_calls()
    {
        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"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-1",
                "thread-1",
                "Interview update",
                "Maria Recruiter <maria@acme.test>",
                "user@example.test",
                DateTimeOffset.UtcNow.AddDays(-1),
                "Snippet 1",
                "Body text 1",
                null,
                Array.Empty<string>(),
                Array.Empty<GmailMessageAttachment>()));
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-2", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-2",
                "thread-1",
                "Interview follow-up",
                "user@example.test",
                "Maria Recruiter <maria@acme.test>",
                DateTimeOffset.UtcNow,
                "Snippet 2",
                "Body text 2",
                null,
                Array.Empty<string>(),
                Array.Empty<GmailMessageAttachment>()));

        var controller = CreateController(db, gmail.Object, "user-1");
        var request = new ImportGmailThreadRequest(job.Id, "thread-1", new[] { "msg-1", "msg-2" });

        var first = await controller.ImportThread(request, CancellationToken.None);
        var firstOk = Assert.IsType<OkObjectResult>(first.Result);
        var firstPayload = Assert.IsType<GmailImportResultDto>(firstOk.Value);
        Assert.Equal(2, firstPayload.Imported);
        Assert.Equal(0, firstPayload.Skipped);
        Assert.Equal("thread-1", firstPayload.ThreadId);

        var second = await controller.ImportThread(request, CancellationToken.None);
        var secondOk = Assert.IsType<OkObjectResult>(second.Result);
        var secondPayload = Assert.IsType<GmailImportResultDto>(secondOk.Value);
        Assert.Equal(0, secondPayload.Imported);
        Assert.Equal(2, secondPayload.Skipped);

        var storedMessages = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).OrderBy(message => message.ExternalMessageId).ToListAsync();
        Assert.Equal(2, storedMessages.Count);
        Assert.All(storedMessages, message => Assert.Equal("thread-1", message.ExternalThreadId));
    }

    [Fact]
    public async Task Refresh_linked_threads_imports_new_messages_for_known_thread_ids()
    {
        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"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = job.Id,
            From = "Company",
            Subject = "Initial recruiter note",
            ExternalMessageId = "msg-1",
            ExternalThreadId = "thread-1",
            ExternalFrom = "Maria Recruiter <maria@acme.test>",
            ExternalTo = "user@example.test",
            Content = "Existing import"
        });
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailMessageSummary("msg-1", "thread-1", "Initial recruiter note", "Maria Recruiter <maria@acme.test>", "user@example.test", DateTimeOffset.UtcNow.AddDays(-2), "Old message"),
                new GmailMessageSummary("msg-2", "thread-1", "Follow-up reply", "user@example.test", "Maria Recruiter <maria@acme.test>", DateTimeOffset.UtcNow, "New reply")
            });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-2", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-2",
                "thread-1",
                "Follow-up reply",
                "user@example.test",
                "Maria Recruiter <maria@acme.test>",
                DateTimeOffset.UtcNow,
                "New reply",
                "Reply body",
                null,
                Array.Empty<string>(),
                Array.Empty<GmailMessageAttachment>()));

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.RefreshLinkedThreads(new RefreshLinkedThreadsRequest(job.Id), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailThreadRefreshResultDto>(ok.Value);
        Assert.Equal(job.Id, payload.JobApplicationId);
        Assert.Equal(1, payload.ThreadsChecked);
        Assert.Equal(1, payload.Imported);
        Assert.Equal(1, payload.Skipped);
        Assert.True(payload.HasLinkedThreads);
        var thread = Assert.Single(payload.Threads);
        Assert.Equal("thread-1", thread.ThreadId);
        Assert.Equal("imported-new-messages", thread.Status);
        Assert.Equal(2, thread.TotalMessages);

        var stored = await db.Correspondences.Where(message => message.JobApplicationId == job.Id).OrderBy(message => message.ExternalMessageId).ToListAsync();
        Assert.Equal(2, stored.Count);
        Assert.Equal("msg-2", stored[1].ExternalMessageId);
        Assert.Equal("thread-1", stored[1].ExternalThreadId);
    }

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

        var linkedJob = new JobApplication { JobTitle = "Backend Developer", CompanyId = company.Id, OwnerUserId = "user-1" };
        var emptyJob = new JobApplication { JobTitle = "Backend Developer II", CompanyId = company.Id, OwnerUserId = "user-1" };
        db.JobApplications.AddRange(linkedJob, emptyJob);
        await db.SaveChangesAsync();

        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = linkedJob.Id,
            From = "Company",
            Subject = "Initial recruiter note",
            ExternalMessageId = "msg-1",
            ExternalThreadId = "thread-1",
            Content = "Existing import"
        });
        await db.SaveChangesAsync();

        var disconnectedGmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
        disconnectedGmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>())).ReturnsAsync((GmailConnection?)null);
        var disconnectedController = CreateController(db, disconnectedGmail.Object, "user-1");
        var disconnectedResult = await disconnectedController.RefreshLinkedThreads(new RefreshLinkedThreadsRequest(linkedJob.Id), CancellationToken.None);
        var conflict = Assert.IsType<ConflictObjectResult>(disconnectedResult.Result);
        Assert.Equal("Connect Gmail before refreshing linked threads.", conflict.Value);

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        var controller = CreateController(db, gmail.Object, "user-1");
        var emptyResult = await controller.RefreshLinkedThreads(new RefreshLinkedThreadsRequest(emptyJob.Id), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(emptyResult.Result);
        var payload = Assert.IsType<GmailThreadRefreshResultDto>(ok.Value);
        Assert.Equal(0, payload.ThreadsChecked);
        Assert.Equal(0, payload.Imported);
        Assert.Equal(0, payload.Skipped);
        Assert.False(payload.HasLinkedThreads);
        Assert.Empty(payload.Threads);
        gmail.Verify(service => service.ListThreadMessagesAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
    }

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

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

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection
            {
                OwnerUserId = "user-1",
                GmailAddress = "user@example.test",
                ConnectedAt = DateTimeOffset.UtcNow.AddDays(-1),
                Scope = "gmail.readonly"
            });
        gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailQueryMatchedMessage(
                    new GmailMessageSummary(
                        "msg-top",
                        "thread-top",
                        "Backend Developer interview",
                        "Maria Recruiter <maria@acme.test>",
                        "user@example.test",
                        DateTimeOffset.UtcNow.AddDays(-2),
                        "Acme wants to schedule a backend developer interview."),
                    new[] { "\"Acme\" \"Backend Developer\" newer_than:365d" })
            });

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.ReviewCandidates(null, 6, CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailReviewQueueResponseDto>(ok.Value);
        Assert.Equal(1, payload.CandidateThreadCount);
        Assert.Single(payload.Threads);
        Assert.Equal("thread-top", payload.Threads[0].ThreadId);
        Assert.True(payload.Threads[0].JobCandidates.Count > 0);
        Assert.Contains(payload.Threads[0].Routing, new[] { "auto-link", "review", "unmatched" });
    }

    [Fact]
    public async Task Refresh_linked_threads_rejects_invalid_job_id()
    {
        await using var db = CreateDb();
        var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
        var controller = CreateController(db, gmail.Object, "user-1");

        var result = await controller.RefreshLinkedThreads(new RefreshLinkedThreadsRequest(0), CancellationToken.None);

        var badRequest = Assert.IsType<BadRequestObjectResult>(result.Result);
        Assert.Equal("Valid jobApplicationId is required.", badRequest.Value);
    }

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

        var foreignJob = new JobApplication
        {
            JobTitle = "Hidden job",
            CompanyId = company.Id,
            OwnerUserId = "user-2"
        };
        db.JobApplications.Add(foreignJob);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.RefreshLinkedThreads(new RefreshLinkedThreadsRequest(foreignJob.Id), CancellationToken.None);

        var notFound = Assert.IsType<NotFoundObjectResult>(result.Result);
        Assert.Equal("Job application not found.", notFound.Value);
        gmail.Verify(service => service.GetConnectionAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
    }

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

        var foreignJob = new JobApplication
        {
            JobTitle = "Hidden job",
            CompanyId = company.Id,
            OwnerUserId = "user-2"
        };
        db.JobApplications.Add(foreignJob);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>(MockBehavior.Strict);
        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.JobCandidates(foreignJob.Id, null, 6, CancellationToken.None);

        var notFound = Assert.IsType<NotFoundObjectResult>(result.Result);
        Assert.Equal("Job application not found.", notFound.Value);
        gmail.Verify(service => service.ListJobCandidateMessagesAsync(It.IsAny<string>(), It.IsAny<IEnumerable<string>>(), It.IsAny<int>(), It.IsAny<CancellationToken>()), Times.Never);
    }

    [Fact]
    public async Task Save_review_decision_links_thread_and_imports_messages()
    {
        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"
        };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailMessageSummary("msg-1", "thread-1", "Backend Developer interview", "Maria Recruiter <maria@acme.test>", "user@example.test", DateTimeOffset.UtcNow.AddDays(-1), "Interview invite")
            });
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-1",
                "thread-1",
                "Backend Developer interview",
                "Maria Recruiter <maria@acme.test>",
                "user@example.test",
                DateTimeOffset.UtcNow.AddDays(-1),
                "Interview invite",
                "Body text",
                null,
                new[] { "INBOX" },
                Array.Empty<GmailMessageAttachment>()));

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.SaveReviewDecision(new SaveGmailReviewDecisionRequest("thread-1", "linked", job.Id, "Strong recruiter match"), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result);
        var decision = await db.GmailReviewDecisions.SingleAsync();
        Assert.Equal("linked", decision.Decision);
        Assert.Equal(job.Id, decision.JobApplicationId);
        Assert.Equal("Strong recruiter match", decision.Note);
        var imported = await db.Correspondences.SingleAsync();
        Assert.Equal("thread-1", imported.ExternalThreadId);
        Assert.Equal("msg-1", imported.ExternalMessageId);
        Assert.NotNull(ok.Value);
    }

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

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

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListJobCandidateMessagesAsync(
                "user-1",
                It.Is<IEnumerable<string>>(queries => queries.Any(query => query.Contains("-in:spam")) && queries.Any(query => query.Contains("-in:trash")) && queries.All(query => query.Contains("newer_than:365d"))),
                8,
                It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailQueryMatchedMessage(
                    new GmailMessageSummary(
                        "msg-1",
                        "thread-1",
                        "Backend Developer interview",
                        "Maria Recruiter <maria@acme.test>",
                        "user@example.test",
                        DateTimeOffset.UtcNow.AddDays(-2),
                        "Acme wants to schedule a backend developer interview."),
                    new[]
                    {
                        "\"Acme\" \"Backend Developer\" newer_than:365d -in:spam -in:trash",
                        "(from:maria@acme.test OR to:maria@acme.test) newer_than:365d -in:spam -in:trash"
                    })
            });
        gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailMessageSummary("msg-1", "thread-1", "Backend Developer interview", "Maria Recruiter <maria@acme.test>", "user@example.test", DateTimeOffset.UtcNow.AddDays(-2), "Invite")
            });
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-1",
                "thread-1",
                "Backend Developer interview",
                "Maria Recruiter <maria@acme.test>",
                "user@example.test",
                DateTimeOffset.UtcNow.AddDays(-2),
                "Invite",
                "Interview details",
                null,
                new[] { "INBOX" },
                Array.Empty<GmailMessageAttachment>()));

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.ManualSync(new GmailManualSyncRequest(365, 8, true, false), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailManualSyncResultDto>(ok.Value);
        Assert.Equal(1, payload.AutoLinkedThreadCount);
        Assert.Equal(1, payload.ImportedThreads);
        Assert.Equal(1, payload.ImportedMessages);
        var decision = await db.GmailReviewDecisions.SingleAsync();
        Assert.Equal("linked", decision.Decision);
        Assert.Equal(job.Id, decision.JobApplicationId);
    }

    [Fact]
    public async Task Suggested_jobs_and_create_suggested_job_create_job_and_link_thread()
    {
        await using var db = CreateDb();
        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
            .ReturnsAsync(Array.Empty<GmailQueryMatchedMessage>());
        gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-suggested", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailMessageSummary("msg-s1", "thread-suggested", "Platform Engineer interview", "Nina Recruiter <nina@beta.test>", "user@example.test", DateTimeOffset.UtcNow.AddDays(-1), "Let's talk about the role")
            });
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-s1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-s1",
                "thread-suggested",
                "Platform Engineer interview",
                "Nina Recruiter <nina@beta.test>",
                "user@example.test",
                DateTimeOffset.UtcNow.AddDays(-1),
                "Let's talk about the role",
                "Interview details",
                null,
                new[] { "INBOX" },
                Array.Empty<GmailMessageAttachment>()));

        db.GmailReviewDecisions.Add(new GmailReviewDecision
        {
            OwnerUserId = "user-1",
            ThreadId = "thread-suggested",
            Decision = "suggested",
            UpdatedAt = DateTimeOffset.UtcNow
        });
        await db.SaveChangesAsync();

        var controller = CreateController(db, gmail.Object, "user-1");

        var reviewQueue = new GmailReviewQueueResponseDto(
            Array.Empty<string>(),
            1,
            0,
            0,
            1,
            new[]
            {
                new GmailReviewThreadDto(
                    "thread-suggested",
                    "Platform Engineer interview",
                    DateTimeOffset.UtcNow.AddDays(-1),
                    1,
                    "suggested",
                    false,
                    null,
                    Array.Empty<string>(),
                    Array.Empty<GmailReviewJobCandidateDto>(),
                    new[]
                    {
                        new GmailJobMatchedMessageDto(
                            "msg-s1",
                            "thread-suggested",
                            "Platform Engineer interview",
                            "Nina Recruiter <nina@beta.test>",
                            "user@example.test",
                            DateTimeOffset.UtcNow.AddDays(-1),
                            "Let's talk about the role",
                            0,
                            "low",
                            false,
                            Array.Empty<string>(),
                            Array.Empty<GmailJobMatchReasonDto>())
                    })
            });

        var suggested = Assert.IsType<OkObjectResult>((await controller.SuggestedJobs(CancellationToken.None)).Result);
        Assert.IsType<GmailSuggestedJobsResponseDto>(suggested.Value);

        var create = await controller.CreateSuggestedJob(new CreateSuggestedGmailJobRequest("thread-suggested", "Beta", "Platform Engineer", "Nina Recruiter", "nina@beta.test", "Create from Gmail suggestion", "Applied"), CancellationToken.None);
        var createOk = Assert.IsType<OkObjectResult>(create.Result);
        var created = Assert.IsType<CreatedSuggestedGmailJobDto>(createOk.Value);
        Assert.True(created.JobApplicationId > 0);
        Assert.Equal(1, created.Imported);
        Assert.Equal("thread-suggested", created.ThreadId);
        Assert.Equal(1, await db.JobApplications.CountAsync());
        var linkedOpportunity = await db.JobApplications.Include(application => application.Job).SingleAsync();
        Assert.NotNull(linkedOpportunity.JobId);
        Assert.Equal("Platform Engineer", linkedOpportunity.Job!.JobTitle);
        Assert.Equal("gmail", linkedOpportunity.Job.Source);
        Assert.Equal(1, await db.Correspondences.CountAsync());
    }

    [Fact]
    public async Task Unlink_thread_removes_messages_and_sets_review_decision()
    {
        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" };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();

        db.Correspondences.AddRange(
            new Correspondence { JobApplicationId = job.Id, From = "Company", Content = "First", ExternalMessageId = "msg-1", ExternalThreadId = "thread-1" },
            new Correspondence { JobApplicationId = job.Id, From = "Me", Content = "Second", ExternalMessageId = "msg-2", ExternalThreadId = "thread-1" });
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<IGmailOAuthService>(), "user-1");
        var result = await controller.UnlinkThread(new UnlinkGmailThreadRequest(job.Id, "thread-1", "Need manual review", "review"), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailUnlinkResultDto>(ok.Value);
        Assert.Equal(2, payload.RemovedMessages);
        Assert.Equal("review", payload.Decision);
        Assert.Empty(await db.Correspondences.ToListAsync());
        var decision = await db.GmailReviewDecisions.SingleAsync();
        Assert.Equal("review", decision.Decision);
        Assert.Equal("Need manual review", decision.Note);
    }

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

        var job = new JobApplication { JobTitle = "Private role", CompanyId = company.Id, OwnerUserId = "user-2" };
        db.JobApplications.Add(job);
        await db.SaveChangesAsync();
        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = job.Id,
            From = "Company",
            Content = "Private message",
            ExternalMessageId = "other-message",
            ExternalThreadId = "other-thread"
        });
        await db.SaveChangesAsync();

        var controller = CreateController(db, Mock.Of<IGmailOAuthService>(), "user-1");
        var result = await controller.UnlinkThread(new UnlinkGmailThreadRequest(job.Id, "other-thread", null, "review"), CancellationToken.None);

        Assert.IsType<NotFoundObjectResult>(result.Result);
        Assert.Single(await db.Correspondences.IgnoreQueryFilters().ToListAsync());
        Assert.Empty(await db.GmailReviewDecisions.IgnoreQueryFilters().ToListAsync());
    }

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

        var sourceJob = new JobApplication { JobTitle = "Source", CompanyId = company.Id, OwnerUserId = "user-1" };
        var targetJob = new JobApplication { JobTitle = "Target", CompanyId = company.Id, OwnerUserId = "user-1" };
        db.JobApplications.AddRange(sourceJob, targetJob);
        await db.SaveChangesAsync();

        db.Correspondences.Add(new Correspondence
        {
            JobApplicationId = sourceJob.Id,
            From = "Company",
            Content = "Existing import",
            ExternalMessageId = "msg-1",
            ExternalThreadId = "thread-1"
        });
        await db.SaveChangesAsync();

        var gmail = new Mock<IGmailOAuthService>();
        gmail.Setup(service => service.ListThreadMessagesAsync("user-1", "thread-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new[]
            {
                new GmailMessageSummary("msg-1", "thread-1", "Interview", "Maria <maria@acme.test>", "user@example.test", DateTimeOffset.UtcNow, "Snippet")
            });
        gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailConnection { OwnerUserId = "user-1", GmailAddress = "user@example.test", EncryptedRefreshToken = "ignored", Scope = "scope", ConnectedAt = DateTimeOffset.UtcNow });
        gmail.Setup(service => service.GetMessageAsync("user-1", "msg-1", It.IsAny<CancellationToken>()))
            .ReturnsAsync(new GmailMessageDetail(
                "msg-1",
                "thread-1",
                "Interview",
                "Maria <maria@acme.test>",
                "user@example.test",
                DateTimeOffset.UtcNow,
                "Snippet",
                "Body",
                null,
                Array.Empty<string>(),
                Array.Empty<GmailMessageAttachment>()));

        var controller = CreateController(db, gmail.Object, "user-1");
        var result = await controller.RelinkThread(new RelinkGmailThreadRequest(targetJob.Id, "thread-1", true, "Move to target"), CancellationToken.None);

        var ok = Assert.IsType<OkObjectResult>(result.Result);
        var payload = Assert.IsType<GmailRelinkResultDto>(ok.Value);
        Assert.Equal(1, payload.UnlinkedMessages);
        Assert.Equal(1, payload.Imported);
        var stored = await db.Correspondences.SingleAsync();
        Assert.Equal(targetJob.Id, stored.JobApplicationId);
        Assert.Equal("thread-1", stored.ExternalThreadId);
        var decision = await db.GmailReviewDecisions.SingleAsync();
        Assert.Equal(targetJob.Id, decision.JobApplicationId);
        Assert.Equal("linked", decision.Decision);
    }

    private static GmailController CreateController(JobTrackerContext db, IGmailOAuthService gmail, string userId)
    {
        var controller = new GmailController(gmail, new GmailJobMatchingService(), db, BuildConfig())
        {
            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 Microsoft.Extensions.Configuration.IConfiguration BuildConfig()
    {
        return new Microsoft.Extensions.Configuration.ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>())
            .Build();
    }
}
