using System.IO.Compression;
using System.Reflection;
using System.Security.Claims;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Tests;

public sealed class AppUpdatesControllerTests
{
    [Fact]
    public void Management_actions_are_restricted_to_super_admin()
    {
        var method = typeof(AppUpdatesController)
            .GetMethod(nameof(AppUpdatesController.UploadRelease));
        var authorize = method?.GetCustomAttribute<AuthorizeAttribute>();

        Assert.NotNull(authorize);
        Assert.Equal(SystemRoles.SuperAdmin, authorize.Roles);
    }

    [Fact]
    public async Task Upload_publish_check_and_download_form_a_complete_flow()
    {
        await using var fixture = await AppUpdatesFixture.CreateAsync();
        var content = CreateBundle("first");

        var uploaded = await fixture.Controller.UploadRelease(
            UploadRequest(content, "1.0.1"),
            CancellationToken.None);
        var created = Assert.IsType<CreatedAtActionResult>(uploaded.Result);
        var release = Assert.IsType<AppUpdateReleaseItem>(created.Value);
        Assert.Equal(AppUpdateReleaseStatus.Draft, release.Status);
        Assert.Equal(64, release.Sha256.Length);

        var publishedAction = await fixture.Controller.PublishRelease(
            release.Id,
            CancellationToken.None);
        var publishedResult = Assert.IsType<OkObjectResult>(
            publishedAction.Result);
        var published = Assert.IsType<AppUpdateReleaseItem>(
            publishedResult.Value);
        Assert.Equal(AppUpdateReleaseStatus.Published, published.Status);

        var checkAction = await fixture.Controller.GetLatest(
            "android",
            "1.0",
            "production",
            "1.0.0",
            CancellationToken.None);
        var checkResult = Assert.IsType<OkObjectResult>(checkAction.Result);
        var check = Assert.IsType<AppUpdateCheckResponse>(checkResult.Value);
        Assert.True(check.Available);
        Assert.Equal("1.0.1", check.Version);
        Assert.Equal(release.Sha256, check.Sha256);
        Assert.Equal($"app-updates/releases/{release.Id}/bundle", check.DownloadUrl);

        var downloadAction = await fixture.Controller.DownloadBundle(
            release.Id,
            CancellationToken.None);
        var download = Assert.IsType<FileContentResult>(downloadAction);
        Assert.Equal(content, download.FileContents);
        Assert.True(download.EnableRangeProcessing);

        var currentAction = await fixture.Controller.GetLatest(
            "android",
            "1.0",
            "production",
            "1.0.1",
            CancellationToken.None);
        var currentResult = Assert.IsType<OkObjectResult>(
            currentAction.Result);
        Assert.False(Assert.IsType<AppUpdateCheckResponse>(
            currentResult.Value).Available);
    }

    [Fact]
    public async Task Publishing_an_archived_release_performs_a_rollback()
    {
        await using var fixture = await AppUpdatesFixture.CreateAsync();
        var first = await UploadAsync(fixture, "1.0.1", "first");
        await fixture.Controller.PublishRelease(first.Id, CancellationToken.None);
        var second = await UploadAsync(fixture, "1.0.2", "second");
        await fixture.Controller.PublishRelease(second.Id, CancellationToken.None);

        Assert.Equal(
            AppUpdateReleaseStatus.Archived,
            (await fixture.Db.AppUpdateReleases.FindAsync(first.Id))!.Status);
        Assert.Equal(
            AppUpdateReleaseStatus.Published,
            (await fixture.Db.AppUpdateReleases.FindAsync(second.Id))!.Status);

        await fixture.Controller.PublishRelease(first.Id, CancellationToken.None);
        fixture.Db.ChangeTracker.Clear();

        Assert.Equal(
            AppUpdateReleaseStatus.Published,
            (await fixture.Db.AppUpdateReleases.FindAsync(first.Id))!.Status);
        Assert.Equal(
            AppUpdateReleaseStatus.Archived,
            (await fixture.Db.AppUpdateReleases.FindAsync(second.Id))!.Status);
    }

    [Fact]
    public async Task Invalid_or_nested_web_bundles_are_rejected()
    {
        await using var fixture = await AppUpdatesFixture.CreateAsync();
        var invalid = await fixture.Controller.UploadRelease(
            UploadRequest("not a zip"u8.ToArray(), "1.0.1"),
            CancellationToken.None);
        Assert.IsType<ObjectResult>(invalid.Result);

        var nested = CreateBundle("nested", "dist/index.html");
        var nestedResult = await fixture.Controller.UploadRelease(
            UploadRequest(nested, "1.0.2"),
            CancellationToken.None);
        var problem = Assert.IsType<ObjectResult>(nestedResult.Result);
        Assert.Contains(
            "根目录",
            Assert.IsType<ValidationProblemDetails>(problem.Value).Detail);
    }

    private static async Task<AppUpdateReleaseItem> UploadAsync(
        AppUpdatesFixture fixture,
        string version,
        string marker)
    {
        var action = await fixture.Controller.UploadRelease(
            UploadRequest(CreateBundle(marker), version),
            CancellationToken.None);
        return Assert.IsType<AppUpdateReleaseItem>(
            Assert.IsType<CreatedAtActionResult>(action.Result).Value);
    }

    private static AppUpdateUploadRequest UploadRequest(
        byte[] content,
        string version)
    {
        var stream = new MemoryStream(content);
        return new AppUpdateUploadRequest
        {
            Bundle = new FormFile(
                stream,
                0,
                content.Length,
                "bundle",
                $"jiaowu-{version}.zip"),
            Platform = "android",
            Channel = "production",
            Version = version,
            NativeVersion = "1.0",
            ReleaseNotes = $"Release {version}"
        };
    }

    private static byte[] CreateBundle(
        string marker,
        string indexPath = "index.html")
    {
        using var output = new MemoryStream();
        using (var archive = new ZipArchive(
                   output,
                   ZipArchiveMode.Create,
                   leaveOpen: true))
        {
            var index = archive.CreateEntry(indexPath);
            using var writer = new StreamWriter(index.Open());
            writer.Write($"<!doctype html><title>{marker}</title>");
        }
        return output.ToArray();
    }

    private sealed class AppUpdatesFixture : IAsyncDisposable
    {
        private AppUpdatesFixture(
            AppDbContext db,
            AppUpdatesController controller)
        {
            Db = db;
            Controller = controller;
        }

        public AppDbContext Db { get; }
        public AppUpdatesController Controller { get; }

        public static async Task<AppUpdatesFixture> CreateAsync()
        {
            var options = new DbContextOptionsBuilder<AppDbContext>()
                .UseSqlite("Data Source=:memory:")
                .Options;
            var db = new AppDbContext(options);
            await db.Database.OpenConnectionAsync();
            await db.Database.EnsureCreatedAsync();
            var controller = new AppUpdatesController(db)
            {
                ControllerContext = new ControllerContext
                {
                    HttpContext = new DefaultHttpContext
                    {
                        User = new ClaimsPrincipal(new ClaimsIdentity(
                            [
                                new Claim(ClaimTypes.Name, "root"),
                                new Claim(
                                    ClaimTypes.Role,
                                    SystemRoles.SuperAdmin)
                            ],
                            "test"))
                    }
                }
            };
            return new AppUpdatesFixture(db, controller);
        }

        public async ValueTask DisposeAsync() => await Db.DisposeAsync();
    }
}
