using System.Text.Json;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Timetables;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace Jiaowu.Api.Tests;

public sealed class AppCacheTests
{
    [Fact]
    public void Statistics_keys_isolate_data_scope_college_and_filters()
    {
        var firstCollege = Guid.NewGuid();
        var secondCollege = Guid.NewGuid();
        var term = Guid.NewGuid().ToString("N");

        var first = AppCacheKeys.Statistics(
            "grades",
            "College",
            firstCollege,
            term,
            "-");
        var otherCollege = AppCacheKeys.Statistics(
            "grades",
            "College",
            secondCollege,
            term,
            "-");
        var otherScope = AppCacheKeys.Statistics(
            "grades",
            "All",
            firstCollege,
            term,
            "-");
        var otherFilter = AppCacheKeys.Statistics(
            "grades",
            "College",
            firstCollege,
            Guid.NewGuid().ToString("N"),
            "-");

        Assert.NotEqual(first, otherCollege);
        Assert.NotEqual(first, otherScope);
        Assert.NotEqual(first, otherFilter);
        Assert.Equal(
            first,
            AppCacheKeys.Statistics(
                " GRADES ",
                "COLLEGE",
                firstCollege,
                term,
                null));
    }

    [Fact]
    public async Task Hybrid_cache_reuses_value_and_tag_invalidation_reloads_source()
    {
        await using var provider = CreateProvider(enabled: true);
        var cache = provider.GetRequiredService<IAppCache>();
        var sourceCalls = 0;
        var key = $"test:{Guid.NewGuid():N}";

        Task<int> Load(CancellationToken _)
        {
            sourceCalls++;
            return Task.FromResult(sourceCalls);
        }

        var first = await cache.GetOrCreateAsync(
            key,
            Load,
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            CancellationToken.None);
        var second = await cache.GetOrCreateAsync(
            key,
            Load,
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            CancellationToken.None);

        Assert.Equal(1, first);
        Assert.Equal(1, second);
        Assert.Equal(1, sourceCalls);

        await cache.RemoveByTagAsync(AppCacheTags.BaseData);
        var afterInvalidation = await cache.GetOrCreateAsync(
            key,
            Load,
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            CancellationToken.None);

        Assert.Equal(2, afterInvalidation);
        Assert.Equal(2, sourceCalls);
    }

    [Fact]
    public async Task Disabled_cache_always_uses_source()
    {
        await using var provider = CreateProvider(enabled: false);
        var cache = provider.GetRequiredService<IAppCache>();
        var sourceCalls = 0;

        Task<int> Load(CancellationToken _)
        {
            sourceCalls++;
            return Task.FromResult(sourceCalls);
        }

        var first = await cache.GetOrCreateAsync(
            "disabled",
            Load,
            AppCacheProfile.ReferenceData,
            [],
            CancellationToken.None);
        var second = await cache.GetOrCreateAsync(
            "disabled",
            Load,
            AppCacheProfile.ReferenceData,
            [],
            CancellationToken.None);

        Assert.Equal(1, first);
        Assert.Equal(2, second);
    }

    [Fact]
    public async Task Timetable_data_round_trips_through_distributed_cache()
    {
        IDistributedCache distributedCache = new SharedDistributedCache(
            new MemoryDistributedCache(
                Options.Create(new MemoryDistributedCacheOptions())));
        var keyPrefix = $"tests:{Guid.NewGuid():N}";
        await using var writer = CreateProvider(true, distributedCache, keyPrefix);
        var source = CreateTimetableData();

        await writer.GetRequiredService<IAppCache>().GetOrCreateAsync(
            "timetable",
            _ => Task.FromResult(source),
            AppCacheProfile.PublishedTimetable,
            [AppCacheTags.Timetables],
            CancellationToken.None);

        await using var reader = CreateProvider(true, distributedCache, keyPrefix);
        var sourceCalled = false;
        var result = await reader.GetRequiredService<IAppCache>().GetOrCreateAsync(
            "timetable",
            _ =>
            {
                sourceCalled = true;
                return Task.FromResult(source);
            },
            AppCacheProfile.PublishedTimetable,
            [AppCacheTags.Timetables],
            CancellationToken.None);

        Assert.False(sourceCalled);
        Assert.Equal(source.Term.Id, result.Term.Id);
        Assert.Equal(source.Plan!.Id, result.Plan!.Id);
        Assert.Equal("缓存测试课程", result.Entries.Single().CourseName);
        Assert.Equal(new TimeOnly(8, 45), result.Slots.Single().EndsAt);
    }

    [Fact]
    public async Task Analytics_json_round_trips_through_distributed_cache()
    {
        IDistributedCache distributedCache = new SharedDistributedCache(
            new MemoryDistributedCache(
                Options.Create(new MemoryDistributedCacheOptions())));
        var keyPrefix = $"tests:{Guid.NewGuid():N}";
        var source = JsonSerializer.SerializeToElement(new
        {
            totals = new { totalCourses = 3 }
        });

        await using (var writer = CreateProvider(
                         true,
                         distributedCache,
                         keyPrefix))
        {
            await writer.GetRequiredService<IAppCache>().GetOrCreateAsync(
                "statistics:courses",
                _ => Task.FromResult(source),
                AppCacheProfile.Analytics,
                [AppCacheTags.Analytics],
                CancellationToken.None);
        }

        await using var reader = CreateProvider(
            true,
            distributedCache,
            keyPrefix);
        var sourceCalled = false;
        var result = await reader.GetRequiredService<IAppCache>()
            .GetOrCreateAsync(
                "statistics:courses",
                _ =>
                {
                    sourceCalled = true;
                    return Task.FromResult(source);
                },
                AppCacheProfile.Analytics,
                [AppCacheTags.Analytics],
                CancellationToken.None);

        Assert.False(sourceCalled);
        Assert.Equal(
            3,
            result.GetProperty("totals")
                .GetProperty("totalCourses")
                .GetInt32());
    }

    private static ServiceProvider CreateProvider(
        bool enabled,
        IDistributedCache? distributedCache = null,
        string? keyPrefix = null)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.AddHybridCache();
        if (distributedCache is not null)
            services.AddSingleton(distributedCache);
        services.AddSingleton(new AppCacheOptions
        {
            Enabled = enabled,
            KeyPrefix = keyPrefix ?? $"tests:{Guid.NewGuid():N}"
        });
        services.AddSingleton<IHostEnvironment>(new TestHostEnvironment());
        services.AddSingleton<IAppCache, HybridAppCache>();
        return services.BuildServiceProvider();
    }

    private static TimetableData CreateTimetableData()
    {
        var termId = Guid.NewGuid();
        var planId = Guid.NewGuid();
        var taskId = Guid.NewGuid();
        return new TimetableData(
            new TimetableTermDto(
                termId,
                "2026-2027 学年第一学期",
                "2026-2027",
                TermSeason.Autumn,
                new DateOnly(2026, 9, 1),
                new DateOnly(2027, 1, 20),
                true),
            new TimetableSubjectDto(
                Guid.NewGuid(),
                "SE202601",
                "软件工程 2026 级 1 班",
                TimetableResourceType.Class,
                2026,
                Guid.NewGuid(),
                "软件工程",
                Guid.NewGuid(),
                "计算机学院",
                null,
                null,
                null),
            null,
            null,
            new TimetablePlanDto(
                planId,
                "正式课表",
                "V1",
                SchedulePlanStatus.Published,
                DateTime.UtcNow,
                DateTime.UtcNow),
            [
                new TimetableSlotDto(
                    1,
                    "第 1 节",
                    new TimeOnly(8, 0),
                    new TimeOnly(8, 45))
            ],
            [
                new TimetableEntryDto(
                    Guid.NewGuid(),
                    taskId,
                    "TASK-001",
                    "缓存测试教学班",
                    "CACHE-01",
                    "缓存测试课程",
                    ["测试教师"],
                    ["软件工程 2026 级 1 班"],
                    "第一教学楼 101",
                    "第一教学楼",
                    "主校区",
                    1,
                    1,
                    2,
                    1,
                    16,
                    WeekPattern.All,
                    null)
            ],
            [],
            []);
    }

    private sealed class TestHostEnvironment : IHostEnvironment
    {
        public string EnvironmentName { get; set; } = Environments.Development;
        public string ApplicationName { get; set; } = nameof(AppCacheTests);
        public string ContentRootPath { get; set; } = AppContext.BaseDirectory;
        public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider();
    }

    private sealed class SharedDistributedCache(IDistributedCache inner)
        : IDistributedCache
    {
        public byte[]? Get(string key) => inner.Get(key);

        public Task<byte[]?> GetAsync(
            string key,
            CancellationToken token = default) =>
            inner.GetAsync(key, token);

        public void Refresh(string key) => inner.Refresh(key);

        public Task RefreshAsync(
            string key,
            CancellationToken token = default) =>
            inner.RefreshAsync(key, token);

        public void Remove(string key) => inner.Remove(key);

        public Task RemoveAsync(
            string key,
            CancellationToken token = default) =>
            inner.RemoveAsync(key, token);

        public void Set(
            string key,
            byte[] value,
            DistributedCacheEntryOptions options) =>
            inner.Set(key, value, options);

        public Task SetAsync(
            string key,
            byte[] value,
            DistributedCacheEntryOptions options,
            CancellationToken token = default) =>
            inner.SetAsync(key, value, options, token);
    }
}
