using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Excititor.Connectors.Abstractions;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Configuration;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.DependencyInjection;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Discovery;
using StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Fetch;
using StellaOps.Excititor.Core;
using System.IO.Abstractions.TestingHelpers;
using Xunit;

namespace StellaOps.Excititor.Connectors.OCI.OpenVEX.Attest.Tests.Connector;

public sealed class OciOpenVexAttestationConnectorTests
{
    [Fact]
    public async Task FetchAsync_WithOfflineBundle_EmitsRawDocument()
    {
        var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"),
        });

        using var cache = new MemoryCache(new MemoryCacheOptions());
        var httpClient = new HttpClient(new StubHttpMessageHandler())
        {
            BaseAddress = new System.Uri("https://registry.example.com/")
        };

        var httpFactory = new SingleClientHttpClientFactory(httpClient);
        var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger<OciAttestationDiscoveryService>.Instance);
        var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger<OciAttestationFetcher>.Instance);

        var connector = new OciOpenVexAttestationConnector(
            discovery,
            fetcher,
            NullLogger<OciOpenVexAttestationConnector>.Instance,
            TimeProvider.System);

        var settingsValues = ImmutableDictionary<string, string>.Empty
            .Add("Images:0:Reference", "registry.example.com/repo/image:latest")
            .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json")
            .Add("Offline:PreferOffline", "true")
            .Add("Offline:AllowNetworkFallback", "false")
            .Add("Cosign:Mode", "None");

        var settings = new VexConnectorSettings(settingsValues);
        await connector.ValidateAsync(settings, CancellationToken.None);

        var sink = new CapturingRawSink();
        var verifier = new CapturingSignatureVerifier();
        var context = new VexConnectorContext(
            Since: null,
            Settings: VexConnectorSettings.Empty,
            RawSink: sink,
            SignatureVerifier: verifier,
            Normalizers: new NoopNormalizerRouter(),
            Services: new Microsoft.Extensions.DependencyInjection.ServiceCollection().BuildServiceProvider());

        var documents = new List<VexRawDocument>();
        await foreach (var document in connector.FetchAsync(context, CancellationToken.None))
        {
            documents.Add(document);
        }

        documents.Should().HaveCount(1);
        sink.Documents.Should().HaveCount(1);
        documents[0].Format.Should().Be(VexDocumentFormat.OciAttestation);
        documents[0].Metadata.Should().ContainKey("oci.attestation.sourceKind").WhoseValue.Should().Be("offline");
        documents[0].Metadata.Should().ContainKey("vex.provenance.sourceKind").WhoseValue.Should().Be("offline");
        documents[0].Metadata.Should().ContainKey("vex.provenance.registryAuthMode").WhoseValue.Should().Be("Anonymous");
        verifier.VerifyCalls.Should().Be(1);
    }

    [Fact]
    public async Task FetchAsync_WithSignatureMetadata_EnrichesProvenance()
    {
        var fileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
        {
            ["/bundles/attestation.json"] = new MockFileData("{\"payload\":\"\",\"payloadType\":\"application/vnd.in-toto+json\",\"signatures\":[{\"sig\":\"\"}]}"),
        });

        using var cache = new MemoryCache(new MemoryCacheOptions());
        var httpClient = new HttpClient(new StubHttpMessageHandler())
        {
            BaseAddress = new System.Uri("https://registry.example.com/")
        };

        var httpFactory = new SingleClientHttpClientFactory(httpClient);
        var discovery = new OciAttestationDiscoveryService(cache, fileSystem, NullLogger<OciAttestationDiscoveryService>.Instance);
        var fetcher = new OciAttestationFetcher(httpFactory, fileSystem, NullLogger<OciAttestationFetcher>.Instance);

        var connector = new OciOpenVexAttestationConnector(
            discovery,
            fetcher,
            NullLogger<OciOpenVexAttestationConnector>.Instance,
            TimeProvider.System);

        var settingsValues = ImmutableDictionary<string, string>.Empty
            .Add("Images:0:Reference", "registry.example.com/repo/image:latest")
            .Add("Images:0:OfflineBundlePath", "/bundles/attestation.json")
            .Add("Offline:PreferOffline", "true")
            .Add("Offline:AllowNetworkFallback", "false")
            .Add("Cosign:Mode", "Keyless")
            .Add("Cosign:Keyless:Issuer", "https://issuer.example.com")
            .Add("Cosign:Keyless:Subject", "subject@example.com");

        var settings = new VexConnectorSettings(settingsValues);
        await connector.ValidateAsync(settings, CancellationToken.None);

        var sink = new CapturingRawSink();
        var verifier = new CapturingSignatureVerifier
        {
            Result = new VexSignatureMetadata(
                type: "cosign",
                subject: "sig-subject",
                issuer: "sig-issuer",
                keyId: "key-id",
                verifiedAt: DateTimeOffset.UtcNow,
                transparencyLogReference: "rekor://entry/123")
        };

        var context = new VexConnectorContext(
            Since: null,
            Settings: VexConnectorSettings.Empty,
            RawSink: sink,
            SignatureVerifier: verifier,
            Normalizers: new NoopNormalizerRouter(),
            Services: new ServiceCollection().BuildServiceProvider());

        var documents = new List<VexRawDocument>();
        await foreach (var document in connector.FetchAsync(context, CancellationToken.None))
        {
            documents.Add(document);
        }

        documents.Should().HaveCount(1);
        var metadata = documents[0].Metadata;
        metadata.Should().Contain("vex.signature.type", "cosign");
        metadata.Should().Contain("vex.signature.subject", "sig-subject");
        metadata.Should().Contain("vex.signature.issuer", "sig-issuer");
        metadata.Should().Contain("vex.signature.keyId", "key-id");
        metadata.Should().ContainKey("vex.signature.verifiedAt");
        metadata.Should().Contain("vex.signature.transparencyLogReference", "rekor://entry/123");
        metadata.Should().Contain("vex.provenance.cosign.mode", "Keyless");
        metadata.Should().Contain("vex.provenance.cosign.issuer", "https://issuer.example.com");
        metadata.Should().Contain("vex.provenance.cosign.subject", "subject@example.com");
        verifier.VerifyCalls.Should().Be(1);
    }

    private sealed class CapturingRawSink : IVexRawDocumentSink
    {
        public List<VexRawDocument> Documents { get; } = new();

        public ValueTask StoreAsync(VexRawDocument document, CancellationToken cancellationToken)
        {
            Documents.Add(document);
            return ValueTask.CompletedTask;
        }
    }

    private sealed class CapturingSignatureVerifier : IVexSignatureVerifier
    {
        public int VerifyCalls { get; private set; }

        public VexSignatureMetadata? Result { get; set; }

        public ValueTask<VexSignatureMetadata?> VerifyAsync(VexRawDocument document, CancellationToken cancellationToken)
        {
            VerifyCalls++;
            return ValueTask.FromResult(Result);
        }
    }

    private sealed class NoopNormalizerRouter : IVexNormalizerRouter
    {
        public ValueTask<VexClaimBatch> NormalizeAsync(VexRawDocument document, CancellationToken cancellationToken)
            => ValueTask.FromResult(new VexClaimBatch(document, ImmutableArray<VexClaim>.Empty, ImmutableDictionary<string, string>.Empty));
    }

    private sealed class SingleClientHttpClientFactory : IHttpClientFactory
    {
        private readonly HttpClient _client;

        public SingleClientHttpClientFactory(HttpClient client)
        {
            _client = client;
        }

        public HttpClient CreateClient(string name) => _client;
    }

    private sealed class StubHttpMessageHandler : HttpMessageHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)
            {
                RequestMessage = request
            });
        }
    }
}
