using System.Text.Json;
using System.Security.Claims;
using Jiaowu.Api.Controllers;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

namespace Jiaowu.Api.Tests;

public sealed class SsoControllerTests
{
    [Fact]
    public async Task Bind_WithValidLocalCredentials_LinksKeycloakIdentityAndLogsIn()
    {
        await using var fixture = await BindingFixture.CreateAsync();
        var code = new string('a', 32);
        await fixture.StoreTicketAsync(code, "keycloak-subject", "external.name");

        var result = await fixture.Controller.Bind(
            new SsoBindRequest(code, fixture.User.UserName!, "LocalUser@123"),
            CancellationToken.None);

        var response = Assert.IsType<LoginResponse>(result.Value);
        Assert.Equal("test-token", response.Token);
        Assert.Equal(fixture.User.Id, response.User.Id);
        var linkedUser = await fixture.UserManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            "keycloak-subject");
        Assert.Equal(fixture.User.Id, linkedUser?.Id);
        Assert.Null(await fixture.Cache.GetStringAsync($"sso:binding:{code}"));
    }

    [Fact]
    public async Task Bind_WithWrongPassword_DoesNotLinkKeycloakIdentity()
    {
        await using var fixture = await BindingFixture.CreateAsync();
        var code = new string('b', 32);
        await fixture.StoreTicketAsync(code, "keycloak-subject", "external.name");

        var result = await fixture.Controller.Bind(
            new SsoBindRequest(code, fixture.User.UserName!, "WrongPassword@123"),
            CancellationToken.None);

        Assert.NotNull(result.Result);
        Assert.Null(await fixture.UserManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            "keycloak-subject"));
        var user = await fixture.UserManager.FindByIdAsync(fixture.User.Id.ToString());
        Assert.Equal(1, user?.AccessFailedCount);
    }

    [Fact]
    public async Task Account_ReportsCurrentKeycloakBindingAndConfiguredCallback()
    {
        await using var fixture = await BindingFixture.CreateAsync();
        Assert.True((await fixture.UserManager.AddLoginAsync(
            fixture.User,
            new UserLoginInfo(
                SsoAuthSchemes.LoginProvider,
                "keycloak-subject",
                "学校统一身份认证"))).Succeeded);
        fixture.SignInController();

        var result = await fixture.Controller.Account();

        var response = Assert.IsType<SsoAccountResponse>(result.Value);
        Assert.True(response.Enabled);
        Assert.True(response.IsBound);
        Assert.Equal(
            "https://jiaowu.example.edu.cn/signin-keycloak",
            response.CallbackUrl);
    }

    [Fact]
    public async Task Unbind_WithCurrentPassword_RemovesOnlyKeycloakLogin()
    {
        await using var fixture = await BindingFixture.CreateAsync();
        Assert.True((await fixture.UserManager.AddLoginAsync(
            fixture.User,
            new UserLoginInfo(
                SsoAuthSchemes.LoginProvider,
                "keycloak-subject",
                "学校统一身份认证"))).Succeeded);
        fixture.SignInController();

        var result = await fixture.Controller.Unbind(
            new SsoUnbindRequest("LocalUser@123"));

        Assert.IsType<NoContentResult>(result);
        Assert.Null(await fixture.UserManager.FindByLoginAsync(
            SsoAuthSchemes.LoginProvider,
            "keycloak-subject"));
        Assert.True(await fixture.UserManager.CheckPasswordAsync(
            fixture.User,
            "LocalUser@123"));
    }

    [Theory]
    [InlineData(null, "/dashboard")]
    [InlineData("", "/dashboard")]
    [InlineData("https://evil.example/path", "/dashboard")]
    [InlineData("//evil.example/path", "/dashboard")]
    [InlineData("/grades?term=2026-1", "/grades?term=2026-1")]
    public void NormalizeReturnUrl_AllowsOnlyLocalApplicationPaths(
        string? value,
        string expected)
    {
        Assert.Equal(expected, SsoController.NormalizeReturnUrl(value));
    }

    private sealed class BindingFixture : IAsyncDisposable
    {
        private readonly SqliteConnection _connection;
        private readonly ServiceProvider _provider;

        private BindingFixture(
            SqliteConnection connection,
            ServiceProvider provider,
            ApplicationUser user,
            UserManager<ApplicationUser> userManager,
            IDistributedCache cache,
            SsoController controller)
        {
            _connection = connection;
            _provider = provider;
            User = user;
            UserManager = userManager;
            Cache = cache;
            Controller = controller;
        }

        public ApplicationUser User { get; }
        public UserManager<ApplicationUser> UserManager { get; }
        public IDistributedCache Cache { get; }
        public SsoController Controller { get; }

        public void SignInController()
        {
            Controller.ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    User = new ClaimsPrincipal(new ClaimsIdentity(
                        [new Claim(ClaimTypes.NameIdentifier, User.Id.ToString())],
                        "test"))
                }
            };
        }

        public static async Task<BindingFixture> CreateAsync()
        {
            var connection = new SqliteConnection("Data Source=:memory:");
            await connection.OpenAsync();
            var services = new ServiceCollection();
            services.AddLogging();
            services.AddDistributedMemoryCache();
            services.AddDbContext<AppDbContext>(options => options.UseSqlite(connection));
            services
                .AddIdentityCore<ApplicationUser>()
                .AddRoles<ApplicationRole>()
                .AddEntityFrameworkStores<AppDbContext>();

            var provider = services.BuildServiceProvider();
            var db = provider.GetRequiredService<AppDbContext>();
            await db.Database.EnsureCreatedAsync();
            var userManager = provider.GetRequiredService<UserManager<ApplicationUser>>();
            var user = new ApplicationUser
            {
                UserName = "local.user",
                DisplayName = "本地用户",
                IsEnabled = true,
                LockoutEnabled = true
            };
            Assert.True((await userManager.CreateAsync(user, "LocalUser@123")).Succeeded);

            var cache = provider.GetRequiredService<IDistributedCache>();
            var controller = new SsoController(
                userManager,
                new StubAuthSessionService(),
                cache,
                Options.Create(new SsoOptions
                {
                    Enabled = true,
                    DisplayName = "学校统一身份认证",
                    CallbackUrl = "https://jiaowu.example.edu.cn/signin-keycloak"
                }),
                NullLogger<SsoController>.Instance);
            return new BindingFixture(
                connection,
                provider,
                user,
                userManager,
                cache,
                controller);
        }

        public Task StoreTicketAsync(
            string code,
            string subject,
            string externalUserName) =>
            Cache.SetStringAsync(
                $"sso:binding:{code}",
                JsonSerializer.Serialize(
                    new SsoBindingTicket(subject, externalUserName)));

        public async ValueTask DisposeAsync()
        {
            await _provider.DisposeAsync();
            await _connection.DisposeAsync();
        }
    }

    private sealed class StubAuthSessionService : IAuthSessionService
    {
        public Task<AuthSessionResult> CreateAsync(
            ApplicationUser user,
            IEnumerable<string> roles,
            AuthenticationClientType clientType,
            CancellationToken cancellationToken = default) =>
            Task.FromResult(new AuthSessionResult(
                "test-token",
                DateTime.UtcNow.AddMinutes(10),
                "test-refresh-token-value-with-sufficient-length",
                DateTime.UtcNow.AddMinutes(30),
                user,
                roles.ToList()));

        public Task<AuthSessionResult?> RefreshAsync(
            string refreshToken,
            CancellationToken cancellationToken = default) => Task.FromResult<AuthSessionResult?>(null);

        public Task RevokeAsync(
            string refreshToken,
            CancellationToken cancellationToken = default) => Task.CompletedTask;
    }
}
