using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using StellaOps.Auth.Abstractions;
using StellaOps.Auth.Client;
using StellaOps.Cli.Configuration;
using StellaOps.Cli.Services;
using StellaOps.Cli.Services.Models;
using StellaOps.Cli.Services.Models.Transport;
using StellaOps.Cli.Tests.Testing;

namespace StellaOps.Cli.Tests.Services;

public sealed class BackendOperationsClientTests
{
    [Fact]
    public async Task DownloadScannerAsync_VerifiesDigestAndWritesMetadata()
    {
        using var temp = new TempDirectory();

        var contentBytes = Encoding.UTF8.GetBytes("scanner-blob");
        var digestHex = Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant();

        var handler = new StubHttpMessageHandler((request, _) =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(contentBytes),
                RequestMessage = request
            };

            response.Headers.Add("X-StellaOps-Digest", $"sha256:{digestHex}");
            response.Content.Headers.LastModified = DateTimeOffset.UtcNow;
            response.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
            return response;
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            ScannerCacheDirectory = temp.Path,
            ScannerDownloadAttempts = 1
        };

        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");
        var result = await client.DownloadScannerAsync("stable", targetPath, overwrite: false, verbose: true, CancellationToken.None);

        Assert.False(result.FromCache);
        Assert.True(File.Exists(targetPath));

        var metadataPath = targetPath + ".metadata.json";
        Assert.True(File.Exists(metadataPath));

        using var document = JsonDocument.Parse(File.ReadAllText(metadataPath));
        Assert.Equal($"sha256:{digestHex}", document.RootElement.GetProperty("digest").GetString());
        Assert.Equal("stable", document.RootElement.GetProperty("channel").GetString());
    }

    [Fact]
    public async Task DownloadScannerAsync_ThrowsOnDigestMismatch()
    {
        using var temp = new TempDirectory();

        var contentBytes = Encoding.UTF8.GetBytes("scanner-data");
        var handler = new StubHttpMessageHandler((request, _) =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.OK)
            {
                Content = new ByteArrayContent(contentBytes),
                RequestMessage = request
            };
            response.Headers.Add("X-StellaOps-Digest", "sha256:deadbeef");
            return response;
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            ScannerCacheDirectory = temp.Path,
            ScannerDownloadAttempts = 1
        };

        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");

        await Assert.ThrowsAsync<InvalidOperationException>(() => client.DownloadScannerAsync("stable", targetPath, overwrite: true, verbose: false, CancellationToken.None));
        Assert.False(File.Exists(targetPath));
    }

    [Fact]
    public async Task DownloadScannerAsync_RetriesOnFailure()
    {
        using var temp = new TempDirectory();

        var successBytes = Encoding.UTF8.GetBytes("success");
        var digestHex = Convert.ToHexString(SHA256.HashData(successBytes)).ToLowerInvariant();
        var attempts = 0;

        var handler = new StubHttpMessageHandler(
            (request, _) =>
            {
                attempts++;
                return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    RequestMessage = request,
                    Content = new StringContent("error")
                };
            },
            (request, _) =>
            {
                attempts++;
                var response = new HttpResponseMessage(HttpStatusCode.OK)
                {
                    RequestMessage = request,
                    Content = new ByteArrayContent(successBytes)
                };
                response.Headers.Add("X-StellaOps-Digest", $"sha256:{digestHex}");
                return response;
            });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            ScannerCacheDirectory = temp.Path,
            ScannerDownloadAttempts = 3
        };

        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        var targetPath = Path.Combine(temp.Path, "scanner.tar.gz");
        var result = await client.DownloadScannerAsync("stable", targetPath, overwrite: false, verbose: false, CancellationToken.None);

        Assert.Equal(2, attempts);
        Assert.False(result.FromCache);
        Assert.True(File.Exists(targetPath));
    }

    [Fact]
    public async Task UploadScanResultsAsync_RetriesOnRetryAfter()
    {
        using var temp = new TempDirectory();
        var filePath = Path.Combine(temp.Path, "scan.json");
        await File.WriteAllTextAsync(filePath, "{}");

        var attempts = 0;
        var handler = new StubHttpMessageHandler(
            (request, _) =>
            {
                attempts++;
                var response = new HttpResponseMessage(HttpStatusCode.TooManyRequests)
                {
                    RequestMessage = request,
                    Content = new StringContent("busy")
                };
                response.Headers.Add("Retry-After", "1");
                return response;
            },
            (request, _) =>
            {
                attempts++;
                return new HttpResponseMessage(HttpStatusCode.OK)
                {
                    RequestMessage = request
                };
            });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            ScanUploadAttempts = 3
        };

        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        await client.UploadScanResultsAsync(filePath, CancellationToken.None);

        Assert.Equal(2, attempts);
    }

    [Fact]
    public async Task UploadScanResultsAsync_ThrowsAfterMaxAttempts()
    {
        using var temp = new TempDirectory();
        var filePath = Path.Combine(temp.Path, "scan.json");
        await File.WriteAllTextAsync(filePath, "{}");

        var attempts = 0;
        var handler = new StubHttpMessageHandler(
            (request, _) =>
            {
                attempts++;
                return new HttpResponseMessage(HttpStatusCode.BadGateway)
                {
                    RequestMessage = request,
                    Content = new StringContent("bad gateway")
                };
            });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            ScanUploadAttempts = 2
        };

        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        await Assert.ThrowsAsync<InvalidOperationException>(() => client.UploadScanResultsAsync(filePath, CancellationToken.None));
        Assert.Equal(2, attempts);
    }

    [Fact]
    public async Task TriggerJobAsync_ReturnsAcceptedResult()
    {
        var handler = new StubHttpMessageHandler((request, _) =>
        {
            var response = new HttpResponseMessage(HttpStatusCode.Accepted)
            {
                RequestMessage = request,
                Content = JsonContent.Create(new JobRunResponse
                {
                    RunId = Guid.NewGuid(),
                    Status = "queued",
                    Kind = "export:json",
                    Trigger = "cli",
                    CreatedAt = DateTimeOffset.UtcNow
                })
            };
            response.Headers.Location = new Uri("/jobs/export:json/runs/123", UriKind.Relative);
            return response;
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions { BackendUrl = "https://feedser.example" };
        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        var result = await client.TriggerJobAsync("export:json", new Dictionary<string, object?>(), CancellationToken.None);

        Assert.True(result.Success);
        Assert.Equal("Accepted", result.Message);
        Assert.Equal("/jobs/export:json/runs/123", result.Location);
    }

    [Fact]
    public async Task TriggerJobAsync_ReturnsFailureMessage()
    {
        var handler = new StubHttpMessageHandler((request, _) =>
        {
            var problem = new
            {
                title = "Job already running",
                detail = "export job active"
            };

            var response = new HttpResponseMessage(HttpStatusCode.Conflict)
            {
                RequestMessage = request,
                Content = JsonContent.Create(problem)
            };
            return response;
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions { BackendUrl = "https://feedser.example" };
        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>());

        var result = await client.TriggerJobAsync("export:json", new Dictionary<string, object?>(), CancellationToken.None);

        Assert.False(result.Success);
        Assert.Contains("Job already running", result.Message);
    }

    [Fact]
    public async Task TriggerJobAsync_UsesAuthorityTokenWhenConfigured()
    {
        using var temp = new TempDirectory();

        var handler = new StubHttpMessageHandler((request, _) =>
        {
            Assert.NotNull(request.Headers.Authorization);
            Assert.Equal("Bearer", request.Headers.Authorization!.Scheme);
            Assert.Equal("token-123", request.Headers.Authorization.Parameter);

            return new HttpResponseMessage(HttpStatusCode.Accepted)
            {
                RequestMessage = request,
                Content = JsonContent.Create(new JobRunResponse
                {
                    RunId = Guid.NewGuid(),
                    Kind = "test",
                    Status = "Pending",
                    Trigger = "cli",
                    CreatedAt = DateTimeOffset.UtcNow
                })
            };
        });

        var httpClient = new HttpClient(handler)
        {
            BaseAddress = new Uri("https://feedser.example")
        };

        var options = new StellaOpsCliOptions
        {
            BackendUrl = "https://feedser.example",
            Authority =
            {
                Url = "https://authority.example",
                ClientId = "cli",
                ClientSecret = "secret",
                Scope = "feedser.jobs.trigger",
                TokenCacheDirectory = temp.Path
            }
        };

        var tokenClient = new StubTokenClient();
        var loggerFactory = LoggerFactory.Create(builder => builder.SetMinimumLevel(LogLevel.Debug));
        var client = new BackendOperationsClient(httpClient, options, loggerFactory.CreateLogger<BackendOperationsClient>(), tokenClient);

        var result = await client.TriggerJobAsync("test", new Dictionary<string, object?>(), CancellationToken.None);

        Assert.True(result.Success);
        Assert.Equal("Accepted", result.Message);
        Assert.True(tokenClient.Requests > 0);
    }

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

        public int Requests { get; private set; }

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

        public ValueTask CacheTokenAsync(string key, StellaOpsTokenCacheEntry entry, CancellationToken cancellationToken = default)
            => ValueTask.CompletedTask;

        public ValueTask ClearCachedTokenAsync(string key, CancellationToken cancellationToken = default)
            => 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<StellaOpsTokenCacheEntry?>(null);

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

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