using System.Text.Json;
using JobTrackerApi.Controllers;
using JobTrackerApi.Data;
using JobTrackerApi.Models;
using JobTrackerApi.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class OperationsControllerTests
{
    [Fact]
    public async Task Operation_api_lists_mutates_and_hides_other_owner_records()
    {
        await using var fixture = await Fixture.CreateAsync();
        Guid ownId;
        Guid otherId;
        await using (var db = fixture.Context("user-1"))
            ownId = (await fixture.Operations(db).CreateAsync(Request("own"), default)).Operation.Id;
        await using (var db = fixture.Context("user-2"))
            otherId = (await fixture.Operations(db).CreateAsync(Request("other"), default)).Operation.Id;

        await using var ownerDb = fixture.Context("user-1");
        var controller = fixture.OperationsController(ownerDb);
        var listed = Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result);
        var item = Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<OperationDto>>(listed.Value));
        Assert.Equal(ownId, item.Id);
        Assert.IsType<NotFoundResult>((await controller.Get(otherId, default)).Result);

        var cancelled = Assert.IsType<OkObjectResult>((await controller.Cancel(ownId, default)).Result);
        Assert.Equal(OperationStatuses.Cancelled, Assert.IsType<OperationDto>(cancelled.Value).Status);
        Assert.IsType<ConflictObjectResult>((await controller.Cancel(ownId, default)).Result);
        var retried = Assert.IsType<OkObjectResult>((await controller.Retry(ownId, default)).Result);
        Assert.Equal(OperationStatuses.Queued, Assert.IsType<OperationDto>(retried.Value).Status);
        Assert.IsType<ConflictObjectResult>((await controller.Retry(ownId, default)).Result);

        var serialized = JsonSerializer.Serialize(item);
        Assert.DoesNotContain("IdempotencyKey", serialized, StringComparison.Ordinal);
        Assert.DoesNotContain("LeaseToken", serialized, StringComparison.Ordinal);
        Assert.DoesNotContain("FailureMessage", serialized, StringComparison.Ordinal);
        Assert.DoesNotContain("ResultReference", serialized, StringComparison.Ordinal);
        Assert.DoesNotContain("Provider", serialized, StringComparison.Ordinal);
        Assert.DoesNotContain("Model", serialized, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Notification_api_is_owner_scoped_and_read_dismiss_are_idempotent()
    {
        await using var fixture = await Fixture.CreateAsync();
        Guid notificationId;
        await using (var db = fixture.Context("user-1"))
        {
            var operations = fixture.Operations(db);
            var operation = await operations.CreateAsync(Request("notification"), default);
            Assert.True(await operations.RequestCancellationAsync(operation.Operation.Id, default));
            notificationId = (await db.UserNotifications.AsNoTracking().SingleAsync()).Id;
        }

        await using (var otherDb = fixture.Context("user-2"))
        {
            var other = fixture.NotificationsController(otherDb);
            Assert.Empty(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
                Assert.IsType<OkObjectResult>((await other.List(cancellationToken: default)).Result).Value));
            Assert.IsType<NotFoundResult>(await other.MarkRead(notificationId, default));
            Assert.IsType<NotFoundResult>(await other.Dismiss(notificationId, default));
        }

        await using var ownerDb = fixture.Context("user-1");
        var controller = fixture.NotificationsController(ownerDb);
        Assert.Single(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
            Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result).Value));
        Assert.IsType<NoContentResult>(await controller.MarkRead(notificationId, default));
        Assert.IsType<NoContentResult>(await controller.MarkRead(notificationId, default));
        Assert.IsType<NoContentResult>(await controller.Dismiss(notificationId, default));
        Assert.IsType<NoContentResult>(await controller.Dismiss(notificationId, default));
        Assert.Empty(Assert.IsAssignableFrom<IReadOnlyList<NotificationDto>>(
            Assert.IsType<OkObjectResult>((await controller.List(cancellationToken: default)).Result).Value));
    }

    [Fact]
    public async Task Operation_and_notification_endpoints_validate_bounds_and_require_local_auth()
    {
        await using var fixture = await Fixture.CreateAsync();
        await using var db = fixture.Context("user-1");
        Assert.IsType<BadRequestObjectResult>((await fixture.OperationsController(db).List(0, default)).Result);
        Assert.IsType<BadRequestObjectResult>((await fixture.NotificationsController(db).List(101, default)).Result);

        foreach (var type in new[] { typeof(OperationsController), typeof(NotificationsController) })
        {
            var authorize = Assert.Single(type.GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true).Cast<AuthorizeAttribute>());
            Assert.Equal("local", authorize.AuthenticationSchemes);
        }
    }

    private static CreateUserOperation Request(string key) => new("synthetic-test", key, "authorized", "local-only", "job", "42");

    private sealed class CurrentUser(string? userId) : ICurrentUserService
    {
        public string? UserId { get; } = userId;
    }

    private sealed class Fixture : IAsyncDisposable
    {
        private readonly string _root;
        private readonly string _connectionString;

        private Fixture(string root)
        {
            _root = root;
            _connectionString = $"Data Source={Path.Combine(root, "api.db")};Default Timeout=5;Pooling=False";
        }

        public static async Task<Fixture> CreateAsync()
        {
            var root = Path.Combine(Path.GetTempPath(), $"jobtracker-operation-api-{Guid.NewGuid():N}");
            Directory.CreateDirectory(root);
            var fixture = new Fixture(root);
            await using var db = fixture.Context(null);
            await db.Database.EnsureCreatedAsync();
            return fixture;
        }

        public JobTrackerContext Context(string? owner)
        {
            var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(_connectionString).Options;
            return new JobTrackerContext(options, new CurrentUser(owner));
        }

        public UserOperationStore Operations(JobTrackerContext db) => new(db, TimeProvider.System);
        public UserNotificationStore Notifications(JobTrackerContext db) => new(db, TimeProvider.System);
        public OperationsController OperationsController(JobTrackerContext db) => new(Operations(db));
        public NotificationsController NotificationsController(JobTrackerContext db) => new(Notifications(db));

        public ValueTask DisposeAsync()
        {
            if (Directory.Exists(_root)) Directory.Delete(_root, true);
            return ValueTask.CompletedTask;
        }
    }
}
