using System.Security.Claims;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using JobTrackerApi.Services.EmailProviders;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class EmailSendControllerTests
{
    [Fact]
    public void Basic_email_send_requires_local_authentication_but_not_pro_entitlement()
    {
        var controllerAuthorization = typeof(EmailController).GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
            .Cast<AuthorizeAttribute>()
            .ToList();
        var sendAuthorization = typeof(EmailController).GetMethod(nameof(EmailController.Send))!
            .GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
            .Cast<AuthorizeAttribute>()
            .ToList();

        var authorization = Assert.Single(controllerAuthorization);
        Assert.Equal("local", authorization.AuthenticationSchemes);
        Assert.True(string.IsNullOrWhiteSpace(authorization.Policy));
        Assert.Empty(sendAuthorization);
    }

    [Fact]
    public async Task Confirmed_send_is_recorded_once_and_duplicate_returns_existing_result()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider();
        var controller = fixture.Controller(provider, "user-1");
        var request = Request(fixture.JobId, confirmed: true);

        var first = await controller.Send(request, default);
        var duplicate = await controller.Send(request, default);

        Assert.Equal(EmailSendStatuses.Sent, Assert.IsType<EmailController.SendResult>(Assert.IsType<OkObjectResult>(first.Result).Value).Status);
        Assert.True(Assert.IsType<EmailController.SendResult>(Assert.IsType<OkObjectResult>(duplicate.Result).Value).Duplicate);
        Assert.Equal(1, provider.SendCount);
        Assert.Equal(1, await fixture.Db.Correspondences.CountAsync());
        Assert.Equal(1, await fixture.Db.JobEvents.CountAsync(item => item.Type == "EmailSent"));
        Assert.DoesNotContain("Body", (await fixture.Db.JobEvents.SingleAsync()).Note ?? string.Empty);
        Assert.Equal(EmailSendStatuses.Sent, (await fixture.Db.EmailSendAttempts.AsNoTracking().SingleAsync()).Status);
    }

    [Fact]
    public async Task Missing_confirmation_or_cross_tenant_job_never_reserves_or_sends()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider();

        var unconfirmed = await fixture.Controller(provider, "user-1").Send(Request(fixture.JobId, confirmed: false), default);
        Assert.IsType<BadRequestObjectResult>(unconfirmed.Result);

        var other = await fixture.Controller(provider, "user-2").Send(Request(fixture.JobId, confirmed: true), default);
        Assert.IsType<NotFoundResult>(other.Result);
        Assert.Equal(0, provider.SendCount);
        Assert.Equal(0, await fixture.Db.EmailSendAttempts.IgnoreQueryFilters().CountAsync());
    }

    [Fact]
    public async Task Known_rejection_is_failed_and_requires_a_new_request_id()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider(_ => throw new EmailProviderDeliveryException("provider_rejected", false, "Rejected"));
        var controller = fixture.Controller(provider, "user-1");
        var request = Request(fixture.JobId, confirmed: true);

        var failed = Assert.IsType<ObjectResult>((await controller.Send(request, default)).Result);
        var duplicate = Assert.IsType<ConflictObjectResult>((await controller.Send(request, default)).Result);

        Assert.Equal(StatusCodes.Status502BadGateway, failed.StatusCode);
        Assert.Equal(EmailSendStatuses.Failed, Assert.IsType<EmailController.SendResult>(failed.Value).Status);
        Assert.Equal(EmailSendStatuses.Failed, Assert.IsType<EmailController.SendResult>(duplicate.Value).Status);
        Assert.Equal(1, provider.SendCount);
        Assert.Empty(await fixture.Db.Correspondences.ToListAsync());
    }

    [Fact]
    public async Task Transport_interruption_is_uncertain_and_is_never_retried()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider(_ => throw new EmailProviderDeliveryException("transport_interrupted", true, "Uncertain"));
        var controller = fixture.Controller(provider, "user-1");
        var request = Request(fixture.JobId, confirmed: true);

        var uncertain = Assert.IsType<ObjectResult>((await controller.Send(request, default)).Result);
        var duplicate = Assert.IsType<ConflictObjectResult>((await controller.Send(request, default)).Result);

        Assert.Equal(StatusCodes.Status409Conflict, uncertain.StatusCode);
        Assert.Equal(EmailSendStatuses.Uncertain, Assert.IsType<EmailController.SendResult>(uncertain.Value).Status);
        Assert.Equal(EmailSendStatuses.Uncertain, Assert.IsType<EmailController.SendResult>(duplicate.Value).Status);
        Assert.Equal(1, provider.SendCount);
    }

    [Fact]
    public async Task Invalid_or_missing_fields_never_reserve_or_send()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider();
        var controller = fixture.Controller(provider, "user-1");

        var invalid = new[]
        {
            Request(fixture.JobId, confirmed: true) with { Provider = null },
            Request(fixture.JobId, confirmed: true) with { ClientRequestId = null },
            Request(fixture.JobId, confirmed: true) with { To = null },
            Request(fixture.JobId, confirmed: true) with { Subject = null },
            Request(fixture.JobId, confirmed: true) with { BodyText = null },
            Request(fixture.JobId, confirmed: true) with { ThreadId = new string('x', 513) },
        };

        foreach (var request in invalid)
            Assert.IsType<BadRequestObjectResult>((await controller.Send(request, default)).Result);

        Assert.Equal(0, provider.SendCount);
        Assert.Equal(0, await fixture.Db.EmailSendAttempts.CountAsync());
    }

    [Fact]
    public async Task Connection_check_failure_is_definite_and_never_calls_send()
    {
        await using var fixture = await Fixture.CreateAsync();
        var provider = new FakeProvider(connectionError: new HttpRequestException("Synthetic connection failure"));
        var controller = fixture.Controller(provider, "user-1");

        var failed = Assert.IsType<ObjectResult>((await controller.Send(Request(fixture.JobId, confirmed: true), default)).Result);

        Assert.Equal(StatusCodes.Status502BadGateway, failed.StatusCode);
        Assert.Equal(EmailSendStatuses.Failed, Assert.IsType<EmailController.SendResult>(failed.Value).Status);
        Assert.Equal(0, provider.SendCount);
        Assert.Equal(EmailSendStatuses.Failed, (await fixture.Db.EmailSendAttempts.AsNoTracking().SingleAsync()).Status);
    }

    private static EmailController.SendRequest Request(int jobId, bool confirmed) => new(
        jobId,
        "gmail",
        "00000000-0000-0000-0000-000000000123",
        "recruiter@example.test",
        "Interview follow-up",
        "Synthetic body",
        "thread-1",
        confirmed);

    private sealed class FakeProvider(Func<EmailDeliveryRequest, EmailDeliveryResult>? send = null, Exception? connectionError = null) : IEmailProvider
    {
        public int SendCount { get; private set; }
        public string ProviderKey => "gmail";
        public Task<EmailConnectionInfo?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken) =>
            connectionError is null
                ? Task.FromResult<EmailConnectionInfo?>(new("gmail", "owner@gmail.test", true))
                : Task.FromException<EmailConnectionInfo?>(connectionError);
        public Task<IReadOnlyList<EmailMessageSummary>> SearchAsync(string ownerUserId, string? query, int maxResults, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<IReadOnlyList<EmailMessageSummary>> ListThreadMessagesAsync(string ownerUserId, string threadId, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<EmailMessageDetail> GetMessageAsync(string ownerUserId, string messageId, CancellationToken cancellationToken) => throw new NotSupportedException();
        public Task<EmailDeliveryResult> SendAsync(string ownerUserId, EmailDeliveryRequest request, CancellationToken cancellationToken)
        {
            SendCount++;
            return Task.FromResult(send?.Invoke(request) ?? new EmailDeliveryResult("message-1", request.ThreadId));
        }
    }

    private sealed class Fixture(SqliteConnection connection, DbContextOptions<JobTrackerContext> options, JobTrackerContext db, int jobId) : IAsyncDisposable
    {
        private readonly List<JobTrackerContext> contexts = new();
        public JobTrackerContext Db { get; } = db;
        public int JobId { get; } = jobId;

        public static async Task<Fixture> CreateAsync()
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options;
            var db = CreateDb(options, "user-1");
            await db.Database.EnsureCreatedAsync();
            var company = new Company { Name = "Acme", OwnerUserId = "user-1" };
            db.Companies.Add(company);
            await db.SaveChangesAsync();
            var job = new JobApplication { JobTitle = "Backend", CompanyId = company.Id, OwnerUserId = "user-1" };
            db.JobApplications.Add(job);
            await db.SaveChangesAsync();
            return new Fixture(connection, options, db, job.Id);
        }

        public EmailController Controller(IEmailProvider provider, string userId)
        {
            var context = userId == "user-1" ? Db : CreateDb(options, userId);
            if (!ReferenceEquals(context, Db)) contexts.Add(context);
            return new EmailController(new EmailProviderRegistry(new[] { provider }), context, new EmailSendAttemptStore(context, TimeProvider.System), NullLogger<EmailController>.Instance)
            {
                ControllerContext = new ControllerContext
                {
                    HttpContext = new DefaultHttpContext
                    {
                        User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test"))
                    }
                }
            };
        }

        private static JobTrackerContext CreateDb(DbContextOptions<JobTrackerContext> options, string userId)
        {
            var currentUser = new Mock<ICurrentUserService>();
            currentUser.SetupGet(service => service.UserId).Returns(userId);
            return new JobTrackerContext(options, currentUser.Object);
        }

        public async ValueTask DisposeAsync()
        {
            foreach (var context in contexts) await context.DisposeAsync();
            await Db.DisposeAsync();
            await connection.DisposeAsync();
        }
    }
}
