using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Commands;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Telemetry;
using StellaOps.Cli.Tests.Testing;
using StellaOps.Cryptography;

namespace StellaOps.Cli.Tests.Commands;

public sealed class CommandHandlersTests
{
    [Fact]
    public async Task HandleExportJobAsync_SetsExitCodeZeroOnSuccess()
    {
        var original = Environment.ExitCode;
        try
        {
            var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", "/jobs/export:json/1", null));
            var provider = BuildServiceProvider(backend);

            await CommandHandlers.HandleExportJobAsync(
                provider,
                format: "json",
                delta: false,
                publishFull: null,
                publishDelta: null,
                includeFull: null,
                includeDelta: null,
                verbose: false,
                cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
            Assert.Equal("export:json", backend.LastJobKind);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleMergeJobAsync_SetsExitCodeOnFailure()
    {
        var original = Environment.ExitCode;
        try
        {
            var backend = new StubBackendClient(new JobTriggerResult(false, "Job already running", null, null));
            var provider = BuildServiceProvider(backend);

            await CommandHandlers.HandleMergeJobAsync(provider, verbose: false, CancellationToken.None);

            Assert.Equal(1, Environment.ExitCode);
            Assert.Equal("merge:reconcile", backend.LastJobKind);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleScannerRunAsync_AutomaticallyUploadsResults()
    {
        using var tempDir = new TempDirectory();
        var resultsFile = Path.Combine(tempDir.Path, "results", "scan.json");
        var backend = new StubBackendClient(new JobTriggerResult(true, "Accepted", null, null));
        var metadataFile = Path.Combine(tempDir.Path, "results", "scan-run.json");
        var executor = new StubExecutor(new ScannerExecutionResult(0, resultsFile, metadataFile));
        var options = new StellaOpsCliOptions
        {
            ResultsDirectory = Path.Combine(tempDir.Path, "results")
        };

        var provider = BuildServiceProvider(backend, executor, new StubInstaller(), options);

        Directory.CreateDirectory(Path.Combine(tempDir.Path, "target"));

        var original = Environment.ExitCode;
        try
        {
            await CommandHandlers.HandleScannerRunAsync(
                provider,
                runner: "docker",
                entry: "scanner-image",
                targetDirectory: Path.Combine(tempDir.Path, "target"),
                arguments: Array.Empty<string>(),
                verbose: false,
                cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
            Assert.Equal(resultsFile, backend.LastUploadPath);
            Assert.True(File.Exists(metadataFile));
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthLoginAsync_UsesClientCredentialsFlow()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    ClientSecret = "secret",
                    Scope = "feedser.jobs.trigger",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var tokenClient = new StubTokenClient();
            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);

            await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
            Assert.Equal(1, tokenClient.ClientCredentialRequests);
            Assert.NotNull(tokenClient.CachedEntry);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthLoginAsync_FailsWhenPasswordMissing()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    Username = "user",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());

            await CommandHandlers.HandleAuthLoginAsync(provider, options, verbose: false, force: false, cancellationToken: CancellationToken.None);

            Assert.Equal(1, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthStatusAsync_ReportsMissingToken()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());

            await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None);

            Assert.Equal(1, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Theory]
    [InlineData(null)]
    [InlineData("default")]
    [InlineData("libsodium")]
    public async Task HandleAuthRevokeVerifyAsync_VerifiesBundlesUsingProviderRegistry(string? providerHint)
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var artifacts = await WriteRevocationArtifactsAsync(tempDir, providerHint);

            await CommandHandlers.HandleAuthRevokeVerifyAsync(
                artifacts.BundlePath,
                artifacts.SignaturePath,
                artifacts.KeyPath,
                verbose: true,
                cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthStatusAsync_ReportsCachedToken()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var tokenClient = new StubTokenClient();
            tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
                "token",
                "Bearer",
                DateTimeOffset.UtcNow.AddMinutes(30),
                new[] { StellaOpsScopes.FeedserJobsTrigger });

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);

            await CommandHandlers.HandleAuthStatusAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthWhoAmIAsync_ReturnsErrorWhenTokenMissing()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: new StubTokenClient());

            await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: false, cancellationToken: CancellationToken.None);

            Assert.Equal(1, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthWhoAmIAsync_ReportsClaimsForJwtToken()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var tokenClient = new StubTokenClient();
            tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
                CreateUnsignedJwt(
                    ("sub", "cli-user"),
                    ("aud", "feedser"),
                    ("iss", "https://authority.example"),
                    ("iat", 1_700_000_000),
                    ("nbf", 1_700_000_000)),
                "Bearer",
                DateTimeOffset.UtcNow.AddMinutes(30),
                new[] { StellaOpsScopes.FeedserJobsTrigger });

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);

            await CommandHandlers.HandleAuthWhoAmIAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);

            Assert.Equal(0, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    [Fact]
    public async Task HandleAuthLogoutAsync_ClearsToken()
    {
        var original = Environment.ExitCode;
        using var tempDir = new TempDirectory();

        try
        {
            var options = new StellaOpsCliOptions
            {
                ResultsDirectory = Path.Combine(tempDir.Path, "results"),
                Authority = new StellaOpsCliAuthorityOptions
                {
                    Url = "https://authority.example",
                    ClientId = "cli",
                    TokenCacheDirectory = tempDir.Path
                }
            };

            var tokenClient = new StubTokenClient();
            tokenClient.CachedEntry = new StellaOpsTokenCacheEntry(
                "token",
                "Bearer",
                DateTimeOffset.UtcNow.AddMinutes(5),
                new[] { StellaOpsScopes.FeedserJobsTrigger });

            var provider = BuildServiceProvider(new StubBackendClient(new JobTriggerResult(true, "ok", null, null)), options: options, tokenClient: tokenClient);

            await CommandHandlers.HandleAuthLogoutAsync(provider, options, verbose: true, cancellationToken: CancellationToken.None);

            Assert.Null(tokenClient.CachedEntry);
            Assert.Equal(1, tokenClient.ClearRequests);
            Assert.Equal(0, Environment.ExitCode);
        }
        finally
        {
            Environment.ExitCode = original;
        }
    }

    private static async Task<RevocationArtifactPaths> WriteRevocationArtifactsAsync(TempDirectory temp, string? providerHint)
    {
        var (bundleBytes, signature, keyPem) = await BuildRevocationArtifactsAsync(providerHint);

        var bundlePath = Path.Combine(temp.Path, "revocation-bundle.json");
        var signaturePath = Path.Combine(temp.Path, "revocation-bundle.json.jws");
        var keyPath = Path.Combine(temp.Path, "revocation-key.pem");

        await File.WriteAllBytesAsync(bundlePath, bundleBytes);
        await File.WriteAllTextAsync(signaturePath, signature);
        await File.WriteAllTextAsync(keyPath, keyPem);

        return new RevocationArtifactPaths(bundlePath, signaturePath, keyPath);
    }

    private static async Task<(byte[] Bundle, string Signature, string KeyPem)> BuildRevocationArtifactsAsync(string? providerHint)
    {
        var bundleBytes = Encoding.UTF8.GetBytes("{\"revocations\":[]}");

        using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
        var parameters = ecdsa.ExportParameters(includePrivateParameters: true);

        var signingKey = new CryptoSigningKey(
            new CryptoKeyReference("revocation-test"),
            SignatureAlgorithms.Es256,
            privateParameters: in parameters,
            createdAt: DateTimeOffset.UtcNow);

        var provider = new DefaultCryptoProvider();
        provider.UpsertSigningKey(signingKey);
        var signer = provider.GetSigner(SignatureAlgorithms.Es256, signingKey.Reference);

        var header = new Dictionary<string, object>
        {
            ["alg"] = SignatureAlgorithms.Es256,
            ["kid"] = signingKey.Reference.KeyId,
            ["typ"] = "application/vnd.stellaops.revocation-bundle+jws",
            ["b64"] = false,
            ["crit"] = new[] { "b64" }
        };

        if (!string.IsNullOrWhiteSpace(providerHint))
        {
            header["provider"] = providerHint;
        }

        var serializerOptions = new JsonSerializerOptions
        {
            PropertyNamingPolicy = null,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
        };

        var headerJson = JsonSerializer.Serialize(header, serializerOptions);
        var encodedHeader = Base64UrlEncoder.Encode(Encoding.UTF8.GetBytes(headerJson));

        var signingInput = new byte[encodedHeader.Length + 1 + bundleBytes.Length];
        var headerBytes = Encoding.ASCII.GetBytes(encodedHeader);
        Buffer.BlockCopy(headerBytes, 0, signingInput, 0, headerBytes.Length);
        signingInput[headerBytes.Length] = (byte)'.';
        Buffer.BlockCopy(bundleBytes, 0, signingInput, headerBytes.Length + 1, bundleBytes.Length);

        var signatureBytes = await signer.SignAsync(signingInput);
        var encodedSignature = Base64UrlEncoder.Encode(signatureBytes);
        var jws = string.Concat(encodedHeader, "..", encodedSignature);

        var publicKeyBytes = ecdsa.ExportSubjectPublicKeyInfo();
        var keyPem = new string(PemEncoding.Write("PUBLIC KEY", publicKeyBytes));

        return (bundleBytes, jws, keyPem);
    }

    private sealed record RevocationArtifactPaths(string BundlePath, string SignaturePath, string KeyPath);

    private static IServiceProvider BuildServiceProvider(
        IBackendOperationsClient backend,
        IScannerExecutor? executor = null,
        IScannerInstaller? installer = null,
        StellaOpsCliOptions? options = null,
        IStellaOpsTokenClient? tokenClient = null)
    {
        var services = new ServiceCollection();
        services.AddSingleton(backend);
        services.AddSingleton<ILoggerFactory>(_ => LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug)));
        services.AddSingleton(new VerbosityState());
        var resolvedOptions = options ?? new StellaOpsCliOptions
        {
            ResultsDirectory = Path.Combine(Path.GetTempPath(), $"stellaops-cli-results-{Guid.NewGuid():N}")
        };
        services.AddSingleton(resolvedOptions);

        var resolvedExecutor = executor ?? CreateDefaultExecutor();
        services.AddSingleton<IScannerExecutor>(resolvedExecutor);
        services.AddSingleton<IScannerInstaller>(installer ?? new StubInstaller());

        if (tokenClient is not null)
        {
            services.AddSingleton(tokenClient);
        }

        return services.BuildServiceProvider();
    }

    private static IScannerExecutor CreateDefaultExecutor()
    {
        var tempResultsFile = Path.GetTempFileName();
        var tempMetadataFile = Path.Combine(
            Path.GetDirectoryName(tempResultsFile)!,
            $"{Path.GetFileNameWithoutExtension(tempResultsFile)}-run.json");
        return new StubExecutor(new ScannerExecutionResult(0, tempResultsFile, tempMetadataFile));
    }

    private sealed class StubBackendClient : IBackendOperationsClient
    {
        private readonly JobTriggerResult _result;

        public StubBackendClient(JobTriggerResult result)
        {
            _result = result;
        }

        public string? LastJobKind { get; private set; }
        public string? LastUploadPath { get; private set; }

        public Task<ScannerArtifactResult> DownloadScannerAsync(string channel, string outputPath, bool overwrite, bool verbose, CancellationToken cancellationToken)
            => throw new NotImplementedException();

        public Task UploadScanResultsAsync(string filePath, CancellationToken cancellationToken)
        {
            LastUploadPath = filePath;
            return Task.CompletedTask;
        }

        public Task<JobTriggerResult> TriggerJobAsync(string jobKind, IDictionary<string, object?> parameters, CancellationToken cancellationToken)
        {
            LastJobKind = jobKind;
            return Task.FromResult(_result);
        }
    }

    private sealed class StubExecutor : IScannerExecutor
    {
        private readonly ScannerExecutionResult _result;

        public StubExecutor(ScannerExecutionResult result)
        {
            _result = result;
        }

        public Task<ScannerExecutionResult> RunAsync(string runner, string entry, string targetDirectory, string resultsDirectory, IReadOnlyList<string> arguments, bool verbose, CancellationToken cancellationToken)
        {
            Directory.CreateDirectory(Path.GetDirectoryName(_result.ResultsPath)!);
            if (!File.Exists(_result.ResultsPath))
            {
                File.WriteAllText(_result.ResultsPath, "{}");
            }

            Directory.CreateDirectory(Path.GetDirectoryName(_result.RunMetadataPath)!);
            if (!File.Exists(_result.RunMetadataPath))
            {
                File.WriteAllText(_result.RunMetadataPath, "{}");
            }

            return Task.FromResult(_result);
        }
    }

    private sealed class StubInstaller : IScannerInstaller
    {
        public Task InstallAsync(string artifactPath, bool verbose, CancellationToken cancellationToken)
            => Task.CompletedTask;
    }

    private sealed class StubTokenClient : IStellaOpsTokenClient
    {
        private readonly StellaOpsTokenResult _token;

        public StubTokenClient()
        {
            _token = new StellaOpsTokenResult(
                "token-123",
                "Bearer",
                DateTimeOffset.UtcNow.AddMinutes(30),
                new[] { StellaOpsScopes.FeedserJobsTrigger });
        }

        public int ClientCredentialRequests { get; private set; }
        public int PasswordRequests { get; private set; }
        public int ClearRequests { get; private set; }
        public StellaOpsTokenCacheEntry? CachedEntry { get; set; }

        public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
        {
            CachedEntry = entry;
            return ValueTask.CompletedTask;
        }

        public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
        {
            ClearRequests++;
            CachedEntry = null;
            return ValueTask.CompletedTask;
        }

        public Task<JsonWebKeySet> GetJsonWebKeySetAsync(CancellationToken cancellationToken = default)
            => Task.FromResult(new JsonWebKeySet("{\"keys\":[]}"));

        public ValueTask<StellaOpsTokenCacheEntry?> GetCachedTokenAsync(string key, CancellationToken cancellationToken = default)
            => ValueTask.FromResult(CachedEntry);

        public Task<StellaOpsTokenResult> RequestClientCredentialsTokenAsync(string? scope = null, CancellationToken cancellationToken = default)
        {
            ClientCredentialRequests++;
            return Task.FromResult(_token);
        }

        public Task<StellaOpsTokenResult> RequestPasswordTokenAsync(string username, string password, string? scope = null, CancellationToken cancellationToken = default)
        {
            PasswordRequests++;
            return Task.FromResult(_token);
        }
    }

    private static string CreateUnsignedJwt(params (string Key, object Value)[] claims)
    {
        var headerJson = "{\"alg\":\"none\",\"typ\":\"JWT\"}";
        var payload = new Dictionary<string, object?>(StringComparer.Ordinal);
        foreach (var claim in claims)
        {
            payload[claim.Key] = claim.Value;
        }

        var payloadJson = JsonSerializer.Serialize(payload);
        return $"{Base64UrlEncode(headerJson)}.{Base64UrlEncode(payloadJson)}.";
    }

    private static string Base64UrlEncode(string value)
    {
        var bytes = Encoding.UTF8.GetBytes(value);
        return Convert.ToBase64String(bytes)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');
    }
}
