using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Services;

namespace StellaOps.Scanner.WebService.Tests;

public sealed class ScansEndpointsTests
{
    [Fact]
    public async Task SubmitScanReturnsAcceptedAndStatusRetrievable()
    {
        using var factory = new ScannerApplicationFactory();
        using var client = factory.CreateClient();

        var request = new ScanSubmitRequest
        {
            Image = new ScanImageDescriptor { Reference = "ghcr.io/demo/app:1.0.0" },
            Force = false
        };

        var response = await client.PostAsJsonAsync("/api/v1/scans", request);
        Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);

        var payload = await response.Content.ReadFromJsonAsync<ScanSubmitResponse>();
        Assert.NotNull(payload);
        Assert.False(string.IsNullOrWhiteSpace(payload!.ScanId));
        Assert.Equal("Pending", payload.Status);
        Assert.True(payload.Created);
        Assert.False(string.IsNullOrWhiteSpace(payload.Location));

        var statusResponse = await client.GetAsync(payload.Location);
        Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode);

        var status = await statusResponse.Content.ReadFromJsonAsync<ScanStatusResponse>();
        Assert.NotNull(status);
        Assert.Equal(payload.ScanId, status!.ScanId);
        Assert.Equal("Pending", status.Status);
        Assert.Equal("ghcr.io/demo/app:1.0.0", status.Image.Reference);
    }

    [Fact]
    public async Task SubmitScanIsDeterministicForIdenticalPayloads()
    {
        using var factory = new ScannerApplicationFactory();
        using var client = factory.CreateClient();

        var request = new ScanSubmitRequest
        {
            Image = new ScanImageDescriptor { Reference = "registry.example.com/acme/app:latest" },
            Force = false,
            ClientRequestId = "client-123",
            Metadata = new Dictionary<string, string> { ["origin"] = "unit-test" }
        };

        var first = await client.PostAsJsonAsync("/api/v1/scans", request);
        var firstPayload = await first.Content.ReadFromJsonAsync<ScanSubmitResponse>();

        var second = await client.PostAsJsonAsync("/api/v1/scans", request);
        var secondPayload = await second.Content.ReadFromJsonAsync<ScanSubmitResponse>();

        Assert.NotNull(firstPayload);
        Assert.NotNull(secondPayload);
        Assert.Equal(firstPayload!.ScanId, secondPayload!.ScanId);
        Assert.True(firstPayload.Created);
        Assert.False(secondPayload.Created);
    }

    [Fact]
    public async Task SubmitScanValidatesImageDescriptor()
    {
        using var factory = new ScannerApplicationFactory();
        using var client = factory.CreateClient();

        var request = new
        {
            image = new { reference = "", digest = "" }
        };

        var response = await client.PostAsJsonAsync("/api/v1/scans", request);
        Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
    }

    [Fact]
    public async Task SubmitScanPropagatesRequestAbortedToken()
    {
        RecordingCoordinator coordinator = null!;
        using var factory = new ScannerApplicationFactory(configuration =>
        {
            configuration["scanner:authority:enabled"] = "false";
        }, services =>
        {
            services.AddSingleton<IScanCoordinator>(sp =>
            {
                coordinator = new RecordingCoordinator(
                    sp.GetRequiredService<IHttpContextAccessor>(),
                    sp.GetRequiredService<TimeProvider>(),
                    sp.GetRequiredService<IScanProgressPublisher>());
                return coordinator;
            });
        });

        using var client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

        var cts = new CancellationTokenSource();
        var request = new ScanSubmitRequest
        {
            Image = new ScanImageDescriptor { Reference = "example.com/demo:1.0" }
        };

        var response = await client.PostAsJsonAsync("/api/v1/scans", request, cts.Token);
        Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);

        Assert.NotNull(coordinator);
        Assert.True(coordinator.TokenMatched);
        Assert.True(coordinator.LastToken.CanBeCanceled);
    }

    private sealed class RecordingCoordinator : IScanCoordinator
    {
        private readonly IHttpContextAccessor accessor;
        private readonly InMemoryScanCoordinator inner;

        public RecordingCoordinator(IHttpContextAccessor accessor, TimeProvider timeProvider, IScanProgressPublisher publisher)
        {
            this.accessor = accessor;
            inner = new InMemoryScanCoordinator(timeProvider, publisher);
        }

        public CancellationToken LastToken { get; private set; }

        public bool TokenMatched { get; private set; }

        public async ValueTask<ScanSubmissionResult> SubmitAsync(ScanSubmission submission, CancellationToken cancellationToken)
        {
            LastToken = cancellationToken;
            TokenMatched = accessor.HttpContext?.RequestAborted.Equals(cancellationToken) ?? false;
            return await inner.SubmitAsync(submission, cancellationToken);
        }

        public ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken)
            => inner.GetAsync(scanId, cancellationToken);
    }

    [Fact]
    public async Task ProgressStreamReturnsInitialPendingEvent()
    {
        using var factory = new ScannerApplicationFactory();
        using var client = factory.CreateClient();

        var request = new ScanSubmitRequest
        {
            Image = new ScanImageDescriptor { Reference = "ghcr.io/demo/app:2.0.0" }
        };

        var submit = await client.PostAsJsonAsync("/api/v1/scans", request);
        var submitPayload = await submit.Content.ReadFromJsonAsync<ScanSubmitResponse>();
        Assert.NotNull(submitPayload);

        var response = await client.GetAsync($"/api/v1/scans/{submitPayload!.ScanId}/events?format=jsonl", HttpCompletionOption.ResponseHeadersRead);
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.Equal("application/x-ndjson", response.Content.Headers.ContentType?.MediaType);

        await using var stream = await response.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        var line = await reader.ReadLineAsync();
        Assert.False(string.IsNullOrWhiteSpace(line));

        var envelope = JsonSerializer.Deserialize<ProgressEnvelope>(line!, SerializerOptions);
        Assert.NotNull(envelope);
        Assert.Equal(submitPayload.ScanId, envelope!.ScanId);
        Assert.Equal("Pending", envelope.State);
        Assert.Equal(1, envelope.Sequence);
        Assert.NotEqual(default, envelope.Timestamp);
    }

    [Fact]
    public async Task ProgressStreamYieldsSubsequentEvents()
    {
        using var factory = new ScannerApplicationFactory();
        using var client = factory.CreateClient();

        var request = new ScanSubmitRequest
        {
            Image = new ScanImageDescriptor { Reference = "registry.example.com/acme/app:stream" }
        };

        var submit = await client.PostAsJsonAsync("/api/v1/scans", request);
        var submitPayload = await submit.Content.ReadFromJsonAsync<ScanSubmitResponse>();
        Assert.NotNull(submitPayload);

        var publisher = factory.Services.GetRequiredService<IScanProgressPublisher>();

        var response = await client.GetAsync($"/api/v1/scans/{submitPayload!.ScanId}/events?format=jsonl", HttpCompletionOption.ResponseHeadersRead);
        await using var stream = await response.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);

        var firstLine = await reader.ReadLineAsync();
        Assert.NotNull(firstLine);
        var firstEnvelope = JsonSerializer.Deserialize<ProgressEnvelope>(firstLine!, SerializerOptions);
        Assert.NotNull(firstEnvelope);
        Assert.Equal("Pending", firstEnvelope!.State);

        _ = Task.Run(async () =>
        {
            await Task.Delay(50);
            publisher.Publish(new ScanId(submitPayload.ScanId), "Running", "worker-started", new Dictionary<string, object?>
            {
                ["stage"] = "download"
            });
        });

        ProgressEnvelope? envelope = null;
        string? line;
        do
        {
            line = await reader.ReadLineAsync();
            if (line is null)
            {
                break;
            }

            if (line.Length == 0)
            {
                continue;
            }

            envelope = JsonSerializer.Deserialize<ProgressEnvelope>(line, SerializerOptions);
        }
        while (envelope is not null && envelope.State == "Pending");

        Assert.NotNull(envelope);
        Assert.Equal("Running", envelope!.State);
        Assert.True(envelope.Sequence >= 2);
        Assert.Contains(envelope.Data.Keys, key => key == "stage");
    }

    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);

    private sealed record ProgressEnvelope(
        string ScanId,
        int Sequence,
        string State,
        string? Message,
        DateTimeOffset Timestamp,
        string CorrelationId,
        Dictionary<string, JsonElement> Data);
}
