using System.Text.Json;
using System.Text;
using System.Threading.Channels;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Nexus.Api.Controllers;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class DomainEventOutboxTests
{
    [Fact]
    public async Task Task_mutation_persists_content_minimized_outbox_event()
    {
        await using var db = CreateDatabase();
        var repository = new TaskRepository(db);
        var task = new WorkTask
        {
            Title = "Sensitive title that must not enter the event payload",
            Detail = "Sensitive details",
            AssignedTo = "programmer"
        };

        await repository.AddAsync(task);

        var created = await db.OutboxEvents.SingleAsync();
        Assert.Equal("task.created", created.Type);
        Assert.Equal("task", created.AggregateType);
        Assert.Equal(task.Id.ToString(), created.AggregateId);
        Assert.DoesNotContain(task.Title, created.PayloadJson, StringComparison.Ordinal);
        Assert.DoesNotContain(task.Detail, created.PayloadJson, StringComparison.Ordinal);

        using var payload = JsonDocument.Parse(created.PayloadJson);
        Assert.Equal(task.Id, payload.RootElement.GetProperty("id").GetGuid());
        Assert.Equal("Backlog", payload.RootElement.GetProperty("state").GetString());

        task.State = "In progress";
        await repository.UpdateAsync(task);

        var events = await db.OutboxEvents
            .OrderBy(item => item.Sequence)
            .ToListAsync();
        Assert.Equal(2, events.Count);
        Assert.Equal("task.updated", events[1].Type);
        using var updatedPayload = JsonDocument.Parse(events[1].PayloadJson);
        Assert.Equal(
            "In progress",
            updatedPayload.RootElement.GetProperty("state").GetString());
    }

    [Fact]
    public async Task Activity_event_keeps_message_content_out_of_domain_stream()
    {
        await using var db = CreateDatabase();
        var repository = new ActivityRepository(db, new LiveUpdateService());
        var taskId = Guid.NewGuid();
        const string sensitiveMessage = "Agent programmer handled private workspace details";

        await repository.AddAsync(new ActivityEvent
        {
            Type = "comment",
            Message = sensitiveMessage,
            TaskId = taskId
        });

        var stored = await db.OutboxEvents.SingleAsync();
        Assert.Equal("activity.created", stored.Type);
        Assert.Equal("activity", stored.AggregateType);
        Assert.DoesNotContain(sensitiveMessage, stored.PayloadJson, StringComparison.Ordinal);
        using var payload = JsonDocument.Parse(stored.PayloadJson);
        Assert.Equal(taskId, payload.RootElement.GetProperty("taskId").GetGuid());
        Assert.Equal("comment", payload.RootElement.GetProperty("type").GetString());
    }

    [Fact]
    public void Outbox_mapper_normalizes_entity_reference_and_clones_payload()
    {
        var stored = new OutboxEvent
        {
            Sequence = 42,
            Type = "agent.proposal.created",
            AggregateType = "AgentProposal",
            AggregateId = Guid.NewGuid().ToString(),
            AggregateRevision = 3,
            PayloadJson = """{"state":"awaiting_approval"}"""
        };

        var mapped = DomainEventStreamService.Map(stored);

        Assert.Equal(42, mapped.Sequence);
        Assert.Equal("agent-proposal", mapped.Entity.Type);
        Assert.Equal(3, mapped.EntityRevision);
        Assert.Equal(
            "awaiting_approval",
            mapped.Payload.GetProperty("state").GetString());
    }

    [Fact]
    public async Task Resync_required_advances_sse_cursor_to_current_sequence()
    {
        await using var db = CreateDatabase();
        db.OutboxEvents.Add(new OutboxEvent
        {
            Sequence = 10,
            Type = "task.updated",
            AggregateType = "task",
            AggregateId = Guid.NewGuid().ToString(),
            PayloadJson = """{"state":"Backlog"}""",
            PublishedAt = DateTimeOffset.UtcNow
        });
        await db.SaveChangesAsync();

        var stream = new FixedDomainEventStream(currentSequence: 25);
        var controller = new DomainEventsController(
            db,
            stream,
            NullLogger<DomainEventsController>.Instance);
        var body = new MemoryStream();
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext()
        };
        controller.Response.Body = body;

        await controller.Get(
            channels: null,
            afterSequence: 1,
            CancellationToken.None);

        body.Position = 0;
        var content = await new StreamReader(
            body,
            Encoding.UTF8,
            leaveOpen: true).ReadToEndAsync();
        Assert.Contains("id: 25", content, StringComparison.Ordinal);
        Assert.Contains("\"eventType\":\"resync_required\"", content, StringComparison.Ordinal);
        Assert.Contains("\"resumeSequence\":25", content, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Replay_exposes_only_events_committed_as_published()
    {
        await using var db = CreateDatabase();
        var entityId = Guid.NewGuid().ToString();
        db.OutboxEvents.AddRange(
            new OutboxEvent
            {
                Sequence = 1,
                Type = "task.updated",
                AggregateType = "task",
                AggregateId = entityId,
                PayloadJson = """{"state":"Review"}""",
                PublishedAt = DateTimeOffset.UtcNow
            },
            new OutboxEvent
            {
                Sequence = 2,
                Type = "task.updated",
                AggregateType = "task",
                AggregateId = entityId,
                PayloadJson = """{"state":"Done"}"""
            });
        await db.SaveChangesAsync();

        var controller = new DomainEventsController(
            db,
            new FixedDomainEventStream(currentSequence: 2),
            NullLogger<DomainEventsController>.Instance);
        var body = new MemoryStream();
        controller.ControllerContext = new ControllerContext
        {
            HttpContext = new DefaultHttpContext()
        };
        controller.Response.Body = body;

        await controller.Get(
            channels: null,
            afterSequence: 0,
            CancellationToken.None);

        body.Position = 0;
        var content = await new StreamReader(
            body,
            Encoding.UTF8,
            leaveOpen: true).ReadToEndAsync();
        Assert.Contains("id: 1", content, StringComparison.Ordinal);
        Assert.Contains("\"state\":\"Review\"", content, StringComparison.Ordinal);
        Assert.DoesNotContain("id: 2", content, StringComparison.Ordinal);
        Assert.DoesNotContain("\"state\":\"Done\"", content, StringComparison.Ordinal);
    }

    private static NexusDbContext CreateDatabase()
    {
        var options = new DbContextOptionsBuilder<NexusDbContext>()
            .UseInMemoryDatabase($"domain-outbox-{Guid.NewGuid():N}")
            .Options;
        return new NexusDbContext(options);
    }

    private sealed class FixedDomainEventStream(long currentSequence)
        : IDomainEventStreamService
    {
        public long CurrentSequence { get; } = currentSequence;

        public DomainEventSubscription Subscribe(IReadOnlySet<string> channels)
        {
            var channel = Channel.CreateUnbounded<DomainEventDto>();
            channel.Writer.TryComplete();
            return new DomainEventSubscription(
                channel.Reader,
                CurrentSequence,
                () => ValueTask.CompletedTask);
        }
    }
}
