using System.Reflection;
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 Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class EmailDraftsControllerTests
{
    [Fact]
    public void Draft_routes_require_local_authentication()
    {
        var authorization = Assert.Single(typeof(EmailDraftsController)
            .GetCustomAttributes<AuthorizeAttribute>());
        Assert.Equal("local", authorization.AuthenticationSchemes);
        Assert.True(string.IsNullOrWhiteSpace(authorization.Policy));
    }

    [Fact]
    public async Task Create_accepts_incomplete_owned_drafts_but_rejects_invalid_or_foreign_inputs()
    {
        await using var fixture = await Fixture.CreateAsync();
        await using var db = fixture.Context("user-1");
        var controller = Controller(db, "user-1");

        var createdResult = await controller.Create(
            new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "GMAIL", "", "", "", null), default);
        var created = Assert.IsType<EmailDraftsController.DraftDto>(
            Assert.IsType<CreatedAtActionResult>(createdResult.Result).Value);
        Assert.Equal("gmail", created.Provider);
        Assert.Equal(1, created.Revision);
        Assert.Empty(created.To);
        Assert.True(Guid.TryParse(created.ClientRequestId, out _));

        Assert.IsType<BadRequestObjectResult>((await controller.Create(
            new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "unknown", "", "", "", null), default)).Result);
        Assert.IsType<BadRequestObjectResult>((await controller.Create(
            new EmailDraftsController.CreateDraftRequest(fixture.UserOneJobId, "gmail", "not-an-address", "", "", null), default)).Result);
        Assert.IsType<NotFoundResult>((await controller.Create(
            new EmailDraftsController.CreateDraftRequest(fixture.UserTwoJobId, "gmail", "", "", "", null), default)).Result);
    }

    [Fact]
    public async Task Direct_ids_and_job_lists_do_not_cross_tenants()
    {
        await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
        await using var db = fixture.Context("user-1");
        var controller = Controller(db, "user-1");

        Assert.IsType<NotFoundResult>((await controller.Get(fixture.UserTwoDraftId, default)).Result);
        var foreignList = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
            Assert.IsType<OkObjectResult>((await controller.List(fixture.UserTwoJobId, default)).Result).Value);
        Assert.Empty(foreignList);
        var ownList = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
            Assert.IsType<OkObjectResult>((await controller.List(fixture.UserOneJobId, default)).Result).Value);
        Assert.Equal(fixture.UserOneDraftId, Assert.Single(ownList).Id);
        var allOwnDrafts = Assert.IsAssignableFrom<IReadOnlyList<EmailDraftsController.DraftDto>>(
            Assert.IsType<OkObjectResult>((await controller.List(null, default)).Result).Value);
        Assert.Equal(fixture.UserOneDraftId, Assert.Single(allOwnDrafts).Id);
    }

    [Fact]
    public async Task Revision_conflicts_prevent_lost_updates_and_cross_tenant_deletes()
    {
        await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
        await using (var userOneDb = fixture.Context("user-1"))
        {
            var userOne = Controller(userOneDb, "user-1");
            var originalRequestId = (await userOneDb.EmailDrafts.AsNoTracking().SingleAsync()).ClientRequestId;
            var updatedResult = await userOne.Update(fixture.UserOneDraftId,
                new EmailDraftsController.UpdateDraftRequest(1, "new@example.test", "Updated", "Updated body"), default);
            var updated = Assert.IsType<EmailDraftsController.DraftDto>(Assert.IsType<OkObjectResult>(updatedResult.Result).Value);
            Assert.Equal(2, updated.Revision);
            Assert.Equal("Updated body", updated.BodyText);
            Assert.Equal(originalRequestId, updated.ClientRequestId);

            Assert.IsType<ConflictObjectResult>((await userOne.Update(fixture.UserOneDraftId,
                new EmailDraftsController.UpdateDraftRequest(1, "stale@example.test", "Stale", "Stale body"), default)).Result);
            Assert.IsType<ConflictObjectResult>(await userOne.Delete(fixture.UserOneDraftId, 1, default));
        }

        await using (var userTwoDb = fixture.Context("user-2"))
        {
            var userTwo = Controller(userTwoDb, "user-2");
            Assert.IsType<NotFoundResult>((await userTwo.Update(fixture.UserOneDraftId,
                new EmailDraftsController.UpdateDraftRequest(2, "other@example.test", "Other", "Other body"), default)).Result);
            Assert.IsType<NotFoundResult>(await userTwo.Delete(fixture.UserOneDraftId, 2, default));
        }

        await using (var userOneDb = fixture.Context("user-1"))
            Assert.IsType<NoContentResult>(await Controller(userOneDb, "user-1").Delete(fixture.UserOneDraftId, 2, default));

        await using var verify = fixture.Context(null);
        var remaining = Assert.Single(await verify.EmailDrafts.IgnoreQueryFilters().AsNoTracking().ToListAsync());
        Assert.Equal("user-2", remaining.OwnerUserId);
    }

    [Fact]
    public async Task New_attempt_identity_requires_the_matching_definitive_failure()
    {
        await using var fixture = await Fixture.CreateAsync(seedDrafts: true);
        await using var db = fixture.Context("user-1");
        var controller = Controller(db, "user-1");
        var original = await db.EmailDrafts.AsNoTracking().SingleAsync();

        var refused = await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default);
        Assert.IsType<ConflictObjectResult>(refused.Result);
        db.EmailSendAttempts.Add(new EmailSendAttempt
        {
            Id = Guid.NewGuid(),
            OwnerUserId = "user-1",
            JobApplicationId = original.JobApplicationId,
            Provider = original.Provider,
            ClientRequestId = original.ClientRequestId,
            PayloadHash = new string('a', 64),
            Status = EmailSendStatuses.Failed,
            FailureCategory = "provider_rejected",
            CreatedAtUtc = DateTime.UtcNow,
            CompletedAtUtc = DateTime.UtcNow,
        });
        await db.SaveChangesAsync();

        var rotatedResult = await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default);
        var rotated = Assert.IsType<EmailDraftsController.DraftDto>(Assert.IsType<OkObjectResult>(rotatedResult.Result).Value);
        Assert.Equal(2, rotated.Revision);
        Assert.NotEqual(original.ClientRequestId, rotated.ClientRequestId);
        Assert.True(Guid.TryParse(rotated.ClientRequestId, out _));

        Assert.IsType<ConflictObjectResult>((await controller.NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(1), default)).Result);
        await using var otherDb = fixture.Context("user-2");
        Assert.IsType<NotFoundResult>((await Controller(otherDb, "user-2").NewAttempt(original.Id, new EmailDraftsController.NewAttemptRequest(2), default)).Result);
    }

    private static EmailDraftsController Controller(JobTrackerContext db, string userId)
    {
        var controller = new EmailDraftsController(
            db,
            new EmailProviderRegistry(new[] { new FakeProvider() }),
            TimeProvider.System);
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext
            {
                User = new ClaimsPrincipal(new ClaimsIdentity(
                    new[] { new Claim(ClaimTypes.NameIdentifier, userId) }, "test")),
            },
        };
        return controller;
    }

    private sealed class FakeProvider : IEmailProvider
    {
        public string ProviderKey => "gmail";
        public Task<EmailConnectionInfo?> GetConnectionAsync(string ownerUserId, CancellationToken cancellationToken) => Task.FromResult<EmailConnectionInfo?>(null);
        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) => throw new NotSupportedException();
    }

    private sealed class Fixture : IAsyncDisposable
    {
        private readonly SqliteConnection _connection;
        private readonly DbContextOptions<JobTrackerContext> _options;

        private Fixture(SqliteConnection connection, DbContextOptions<JobTrackerContext> options)
        {
            _connection = connection;
            _options = options;
        }

        public int UserOneJobId { get; private set; }
        public int UserTwoJobId { get; private set; }
        public Guid UserOneDraftId { get; private set; }
        public Guid UserTwoDraftId { get; private set; }

        public static async Task<Fixture> CreateAsync(bool seedDrafts = false)
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options;
            var fixture = new Fixture(connection, options);
            await using var db = fixture.Context(null);
            await db.Database.EnsureCreatedAsync();
            var companyOne = new Company { Name = "One", OwnerUserId = "user-1" };
            var companyTwo = new Company { Name = "Two", OwnerUserId = "user-2" };
            db.Companies.AddRange(companyOne, companyTwo);
            await db.SaveChangesAsync();
            var jobOne = new JobApplication { JobTitle = "One", CompanyId = companyOne.Id, OwnerUserId = "user-1" };
            var jobTwo = new JobApplication { JobTitle = "Two", CompanyId = companyTwo.Id, OwnerUserId = "user-2" };
            db.JobApplications.AddRange(jobOne, jobTwo);
            await db.SaveChangesAsync();
            fixture.UserOneJobId = jobOne.Id;
            fixture.UserTwoJobId = jobTwo.Id;
            if (seedDrafts)
            {
                var one = Draft("user-1", jobOne.Id, "one@example.test");
                var two = Draft("user-2", jobTwo.Id, "two@example.test");
                db.EmailDrafts.AddRange(one, two);
                await db.SaveChangesAsync();
                fixture.UserOneDraftId = one.Id;
                fixture.UserTwoDraftId = two.Id;
            }
            return fixture;
        }

        public JobTrackerContext Context(string? userId)
        {
            var currentUser = new Mock<ICurrentUserService>();
            currentUser.SetupGet(service => service.UserId).Returns(userId);
            return new JobTrackerContext(_options, currentUser.Object);
        }

        public ValueTask DisposeAsync() => _connection.DisposeAsync();

        private static EmailDraft Draft(string ownerUserId, int jobApplicationId, string recipient) => new()
        {
            Id = Guid.NewGuid(),
            OwnerUserId = ownerUserId,
            JobApplicationId = jobApplicationId,
            Provider = "gmail",
            To = recipient,
            Subject = "Synthetic",
            BodyText = "Synthetic private draft.",
            ClientRequestId = Guid.NewGuid().ToString("D"),
            CreatedAtUtc = DateTime.UtcNow,
            UpdatedAtUtc = DateTime.UtcNow,
        };
    }
}
