using System.Text.Json;
using ClosedXML.Excel;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Tests;

public sealed class StatisticsControllerTests
{
    [Fact]
    public async Task Course_summary_cache_is_scope_isolated_and_export_is_fresh()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var firstCollege = new College { Code = "C01", Name = "第一学院" };
        var secondCollege = new College { Code = "C02", Name = "第二学院" };
        var category = new CourseCategory { Code = "CAT", Name = "测试分类" };
        db.AddRange(
            firstCollege,
            secondCollege,
            category,
            CreateCourse("C001", firstCollege, category),
            CreateCourse("C002", secondCollege, category));
        await db.SaveChangesAsync();

        var cache = new RecordingCache();
        var firstController = new StatisticsController(
            db,
            new CollegeDataScope(firstCollege.Id),
            cache);
        var secondController = new StatisticsController(
            db,
            new CollegeDataScope(secondCollege.Id),
            cache);

        var first = await firstController.GetCourseSummary(
            null,
            null,
            null,
            CancellationToken.None);
        Assert.Equal(1, TotalCourses(first));
        Assert.Equal(
            new[]
            {
                AppCacheTags.Analytics,
                AppCacheTags.BaseData,
                AppCacheTags.Timetables
            },
            cache.Tags.Single());

        db.Courses.Add(CreateCourse("C003", firstCollege, category));
        await db.SaveChangesAsync();

        var cached = await firstController.GetCourseSummary(
            null,
            null,
            null,
            CancellationToken.None);
        Assert.Equal(1, TotalCourses(cached));

        var otherCollege = await secondController.GetCourseSummary(
            null,
            null,
            null,
            CancellationToken.None);
        Assert.Equal(1, TotalCourses(otherCollege));
        Assert.Equal(2, cache.SourceCalls);
        Assert.Equal(2, cache.Keys.Count);

        var export = await firstController.ExportCourses(
            null,
            null,
            null,
            CancellationToken.None);
        var file = Assert.IsType<FileContentResult>(export);
        using var stream = new MemoryStream(file.FileContents);
        using var workbook = new XLWorkbook(stream);
        Assert.Equal(
            2,
            workbook.Worksheet("课程性质").Cell(1, 2).GetValue<int>());
        Assert.Equal(2, cache.SourceCalls);
    }

    [Fact]
    public async Task Course_summary_keeps_request_scope_when_cache_factory_loses_http_context()
    {
        await using var connection = new SqliteConnection("Data Source=:memory:");
        await connection.OpenAsync();
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseSqlite(connection)
            .Options;
        await using var db = new AppDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var college = new College { Code = "C01", Name = "第一学院" };
        var category = new CourseCategory { Code = "CAT", Name = "测试分类" };
        db.AddRange(college, category, CreateCourse("C001", college, category));
        await db.SaveChangesAsync();

        var scope = new MutableDataScope(college.Id);
        var cache = new RecordingCache(() => scope.LoseHttpContext());
        var controller = new StatisticsController(db, scope, cache);

        var result = await controller.GetCourseSummary(
            college.Id,
            null,
            null,
            CancellationToken.None);

        Assert.Equal(1, TotalCourses(result));
        Assert.Equal(1, cache.SourceCalls);
    }

    private static int TotalCourses(ActionResult<object> result)
    {
        var json = Assert.IsType<JsonElement>(result.Value);
        return json.GetProperty("totals").GetProperty("totalCourses").GetInt32();
    }

    private static Course CreateCourse(
        string code,
        College college,
        CourseCategory category) =>
        new()
        {
            Code = code,
            Name = $"课程 {code}",
            CollegeId = college.Id,
            CourseCategoryId = category.Id,
            Nature = CourseNature.MajorRequired,
            Credits = 2,
            TotalHours = 32,
            LectureHours = 32,
            AssessmentMethod = AssessmentMethod.Examination
        };

    private sealed class RecordingCache(Action? beforeFactory = null) : IAppCache
    {
        private readonly Dictionary<string, object> values = [];

        public int SourceCalls { get; private set; }
        public IReadOnlyCollection<string> Keys => values.Keys;
        public List<IReadOnlyCollection<string>> Tags { get; } = [];

        public async Task<T> GetOrCreateAsync<T>(
            string key,
            Func<CancellationToken, Task<T>> factory,
            AppCacheProfile profile,
            IReadOnlyCollection<string> tags,
            CancellationToken cancellationToken)
        {
            if (values.TryGetValue(key, out var value))
                return (T)value;

            SourceCalls++;
            Tags.Add(tags.ToArray());
            beforeFactory?.Invoke();
            var loaded = await factory(cancellationToken);
            values[key] = loaded!;
            return loaded;
        }

        public ValueTask RemoveByTagAsync(
            string tag,
            CancellationToken cancellationToken = default) =>
            ValueTask.CompletedTask;
    }

    private sealed class CollegeDataScope(Guid collegeId) : ICurrentUserDataScope
    {
        public CurrentUserScope Current { get; } = new(
            Guid.NewGuid(),
            "学院管理员",
            collegeId,
            DataScope.College,
            new HashSet<string> { SystemRoles.CollegeAdmin });
    }

    private sealed class MutableDataScope(Guid collegeId) : ICurrentUserDataScope
    {
        private CurrentUserScope current = CreateCollegeScope(collegeId);

        public CurrentUserScope Current => current;

        public void LoseHttpContext()
        {
            current = new CurrentUserScope(
                Guid.Empty,
                null,
                null,
                DataScope.Self,
                new HashSet<string>());
        }

        private static CurrentUserScope CreateCollegeScope(Guid value) =>
            new(
                Guid.NewGuid(),
                "学院管理员",
                value,
                DataScope.College,
                new HashSet<string> { SystemRoles.CollegeAdmin });
    }
}
