using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Tiku.Api.Contracts;
using Tiku.Application.Auth;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Auth;
using Tiku.Infrastructure.Commerce;
using Tiku.Infrastructure.Persistence;

namespace Tiku.IntegrationTests.Api;

public sealed class AuthEndpointTests
{
    [Fact]
    public async Task Sms_send_creates_login_code_without_exposing_it_and_rejects_platform_realm()
    {
        var provider = new CapturingSmsProvider();
        await using var factory = new ApiTestFactory(smsProvider: provider);
        var tenantId = Guid.NewGuid();
        await factory.SeedAsync(new Tenant
        {
            Id = tenantId,
            Slug = tenantId.ToString("N"),
            Name = "SMS Tenant"
        });
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", tenantId.ToString("N"));

        var response = await client.PostAsJsonAsync(
            "/api/tenant/auth/sms/send",
            new SendSmsCodeDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = tenantId.ToString("N"),
                Phone = "13800000000",
                DeviceId = "sms-endpoint-device"
            });
        var body = await response.Content.ReadAsStringAsync();

        Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
        Assert.DoesNotContain(provider.Code!, body, StringComparison.Ordinal);
        Assert.Matches("^[0-9]{6}$", provider.Code!);
        using (var scope = factory.CreateSystemScope("Verify SMS send endpoint"))
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
            var dimensions = await dbContext.SmsSendRateLimits
                .Select(item => item.Dimension)
                .ToArrayAsync();
            Assert.Contains(SmsRateLimitDimension.Tenant, dimensions);
            Assert.Contains(SmsRateLimitDimension.Phone, dimensions);
            Assert.Contains(SmsRateLimitDimension.Device, dimensions);
        }

        client.DefaultRequestHeaders.Remove("x-tenant-code");
        var platformResponse = await client.PostAsJsonAsync(
            "/api/tenant/auth/sms/send",
            new SendSmsCodeDto
            {
                Realm = AuthRealm.Platform,
                Phone = "13800000000",
                DeviceId = "sms-endpoint-device"
            });
        Assert.Equal(HttpStatusCode.BadRequest, platformResponse.StatusCode);
        Assert.Equal(1, provider.SendCount);
    }

    [Fact]
    public async Task Custom_host_rejects_jwt_from_another_tenant_and_ignores_spoofed_tenant_header()
    {
        await using var factory = new ApiTestFactory();
        var tenantB = await SeedLoginUserAsync(factory);
        var tenantAId = Guid.NewGuid();
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantAId,
                Slug = "tenant-a",
                Name = "Tenant A",
                Status = TenantStatus.Active,
                Mode = TenantMode.Saas
            },
            new TenantDomain
            {
                TenantId = tenantAId,
                Host = "a.example.test",
                DomainType = TenantDomainType.Custom,
                Status = TenantDomainStatus.Active,
                IsPrimary = true
            });
        using var client = factory.CreateClient();
        var tokens = await client.LoginAsTenantAsync(tenantB.TenantId, tenantB.Phone);

        using var jwtRequest = new HttpRequestMessage(HttpMethod.Get, "/api/tenant/me");
        jwtRequest.Headers.Host = "a.example.test";
        jwtRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
        var jwtResponse = await client.SendAsync(jwtRequest);

        using var spoofRequest = new HttpRequestMessage(HttpMethod.Post, "/api/tenant/auth/login/password");
        spoofRequest.Headers.Host = "a.example.test";
        spoofRequest.Headers.Add("x-tenant-code", tenantB.TenantId.ToString("N"));
        spoofRequest.Content = JsonContent.Create(new PasswordLoginDto
        {
            Realm = AuthRealm.Tenant,
            Phone = tenantB.Phone,
            Password = PasswordTestUserExtensions.TestPassword
        });
        var spoofResponse = await client.SendAsync(spoofRequest);

        Assert.Equal(HttpStatusCode.Forbidden, jwtResponse.StatusCode);
        Assert.NotEqual(HttpStatusCode.OK, spoofResponse.StatusCode);
    }

    [Fact]
    public async Task Platform_authentication_artifacts_are_rejected_on_an_unconfigured_host()
    {
        await using var factory = new ApiTestFactory(configurationOverrides: new Dictionary<string, string?>
        {
            ["Tenancy:Resolution:ExemptPathPrefixes:3"] = "/api/tenant/auth"
        });
        using var client = factory.CreateClient();
        var refreshToken = $"v2.p.-.{Guid.NewGuid():N}.{new string('a', 86)}";
        var requests = new[]
        {
            new HttpRequestMessage(HttpMethod.Post, "/api/tenant/auth/login/password")
            {
                Content = JsonContent.Create(new PasswordLoginDto
                {
                    Realm = AuthRealm.Platform,
                    Phone = "admin@example.com",
                    Password = PasswordTestUserExtensions.TestPassword
                })
            },
            new HttpRequestMessage(HttpMethod.Post, "/api/tenant/auth/refresh")
            {
                Content = JsonContent.Create(new RefreshSessionDto { RefreshToken = refreshToken })
            },
            new HttpRequestMessage(HttpMethod.Post, "/api/tenant/auth/logout")
            {
                Content = JsonContent.Create(new RefreshSessionDto { RefreshToken = refreshToken })
            }
        };

        foreach (var request in requests)
            using (request)
            {
                request.Headers.Host = "unconfigured.example.test";
                using var response = await client.SendAsync(request);
                Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
            }
    }

    [Fact]
    public async Task Password_login_can_access_current_user_and_tenant()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedLoginUserAsync(factory);
        using var client = factory.CreateClient();

        var tokens = await client.LoginAsTenantAsync(seed.TenantId, seed.Phone);
        client.UseAccessToken(tokens);
        var meResponse = await client.GetAsync("/api/tenant/me");
        var tenantResponse = await client.GetAsync("/api/tenant/context/current");

        Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, tenantResponse.StatusCode);
        Assert.Contains(seed.UserId.ToString(), await meResponse.Content.ReadAsStringAsync(),
            StringComparison.OrdinalIgnoreCase);
        Assert.Contains(seed.TenantId.ToString(), await tenantResponse.Content.ReadAsStringAsync(),
            StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Sms_login_can_access_current_user()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedLoginUserAsync(factory);
        await SeedSmsCodeAsync(factory, seed.TenantId, seed.Phone, "123456");
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", seed.TenantId.ToString("N"));

        var loginResponse = await client.PostAsJsonAsync(
            "/api/tenant/auth/login/sms",
            new SmsLoginDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = seed.TenantId.ToString("N"),
                Phone = seed.Phone,
                Code = "123456"
            });
        var tokens = await client.CompleteTenantAuthenticationAsync(
            loginResponse,
            seed.TenantId,
            seed.Phone);
        client.UseAccessToken(tokens);
        var meResponse = await client.GetAsync("/api/tenant/me");

        Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
    }

    [Fact]
    public async Task Logout_revokes_access_and_refresh_tokens()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedLoginUserAsync(factory);
        using var client = factory.CreateClient();
        var tokens = await client.LoginAsTenantAsync(seed.TenantId, seed.Phone);

        var logoutResponse = await client.PostAsJsonAsync(
            "/api/tenant/auth/logout",
            new RefreshSessionDto { RefreshToken = tokens.RefreshToken });
        client.UseAccessToken(tokens);
        var meResponse = await client.GetAsync("/api/tenant/me");
        var refreshResponse = await client.PostAsJsonAsync(
            "/api/tenant/auth/refresh",
            new RefreshSessionDto { RefreshToken = tokens.RefreshToken });

        Assert.Equal(HttpStatusCode.NoContent, logoutResponse.StatusCode);
        Assert.Equal(HttpStatusCode.Unauthorized, meResponse.StatusCode);
        Assert.Equal(HttpStatusCode.Unauthorized, refreshResponse.StatusCode);
    }

    [Fact]
    public async Task Wechat_miniapp_login_can_access_current_user()
    {
        await using var factory = new ApiTestFactory(new FakeWechatOAuthClient());
        var tenantId = Guid.NewGuid();
        var secretRef = "tenant_secrets:identity:wechat_miniapp:default";
        var protectedSecret = ProtectTenantSecret(
            tenantId,
            secretRef,
            JsonSerializer.SerializeToElement(new { appSecret = "wx-app-secret" }));
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Wechat Tenant"
            },
            new TenantExternalProvider
            {
                TenantId = tenantId,
                Provider = "wechat_miniapp",
                Capability = TenantExternalProviderCapability.Identity,
                Status = TenantExternalProviderStatus.Active,
                SecretRef = secretRef,
                ConfigPublic = JsonSerializer.SerializeToElement(new
                {
                    appId = "wx-app-id"
                })
            },
            new TenantSecret
            {
                TenantId = tenantId,
                Purpose = "identity",
                Provider = "wechat_miniapp",
                SecretKey = "default",
                SecretRef = secretRef,
                Status = TenantSecretStatus.Active,
                EncryptionKeyId = protectedSecret.KeyId,
                EncryptedPayload = protectedSecret.Ciphertext,
                EncryptionNonce = protectedSecret.Nonce,
                EncryptionTag = protectedSecret.Tag
            });
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", tenantId.ToString("N"));

        var loginResponse = await client.PostAsJsonAsync(
            "/api/tenant/auth/oauth/wechat-miniapp",
            new OAuthCodeDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = tenantId.ToString("N"),
                Code = "wx-code"
            });
        var loginJson = await ReadJsonAsync(loginResponse);
        var accessToken = loginJson.RootElement
            .GetProperty("user")
            .GetProperty("tokens")
            .GetProperty("accessToken")
            .GetString();

        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        var meResponse = await client.GetAsync("/api/tenant/me");
        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();

        Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
        Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
        Assert.Contains(dbContext.UserIdentities, identity =>
            identity.Provider == "wechat_miniapp" &&
            identity.OpenId == "mini-open-id" &&
            identity.UnionId == "union-id");
        var persistedUser = dbContext.Users.Single(user =>
            dbContext.UserIdentities.Any(identity =>
                identity.UserId == user.Id && identity.Provider == "wechat_miniapp"));
        Assert.DoesNotContain(
            "session_key",
            persistedUser.RawProfile.GetRawText(),
            StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task Wechat_first_login_is_rejected_without_self_registration_and_leaves_no_identity()
    {
        await using var factory = new ApiTestFactory(new FakeWechatOAuthClient());
        var tenantId = Guid.NewGuid();
        await SeedWechatProviderAsync(factory, tenantId,
            new TenantAuthPolicy
            {
                TenantId = tenantId,
                AllowExternalStudentSelfRegistration = false
            });
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", tenantId.ToString("N"));

        var response = await client.PostAsJsonAsync(
            "/api/tenant/auth/oauth/wechat-miniapp",
            new OAuthCodeDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = tenantId.ToString("N"),
                Code = "wx-code"
            });
        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        Assert.DoesNotContain(dbContext.UserIdentities, item => item.Provider == "wechat_miniapp");
        Assert.DoesNotContain(dbContext.TenantMemberships, item => item.TenantId == tenantId);
        Assert.DoesNotContain(dbContext.AuthSessions, item => item.TenantId == tenantId);
    }

    [Theory]
    [InlineData(MembershipStatus.Invited)]
    [InlineData(MembershipStatus.Disabled)]
    public async Task Wechat_login_does_not_reactivate_non_active_membership(MembershipStatus status)
    {
        await using var factory = new ApiTestFactory(new FakeWechatOAuthClient());
        var tenantId = Guid.NewGuid();
        var user = new User { Id = Guid.NewGuid(), Name = "Existing Wechat User" };
        await SeedWechatProviderAsync(factory, tenantId,
            user,
            new UserIdentity
            {
                UserId = user.Id,
                Provider = "wechat_miniapp",
                ProviderSubject = "wx-app-id:mini-open-id",
                OpenId = "mini-open-id",
                UnionId = "union-id"
            },
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = user.Id,
                Role = TenantRole.Student,
                Status = status
            });
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Add("x-tenant-code", tenantId.ToString("N"));

        var response = await client.PostAsJsonAsync(
            "/api/tenant/auth/oauth/wechat-miniapp",
            new OAuthCodeDto
            {
                Realm = AuthRealm.Tenant,
                TenantCode = tenantId.ToString("N"),
                Code = "wx-code"
            });
        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
        Assert.Equal(status, dbContext.TenantMemberships.Single(item =>
            item.TenantId == tenantId && item.UserId == user.Id).Status);
        Assert.DoesNotContain(dbContext.AuthSessions, item =>
            item.TenantId == tenantId && item.UserId == user.Id);
    }

    private static async Task SeedWechatProviderAsync(
        ApiTestFactory factory,
        Guid tenantId,
        params object[] additionalEntities)
    {
        const string secretRef = "tenant_secrets:identity:wechat_miniapp:default";
        var protectedSecret = ProtectTenantSecret(
            tenantId,
            secretRef,
            JsonSerializer.SerializeToElement(new { appSecret = "wx-app-secret" }));
        var entities = new List<object>
        {
            new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Wechat Tenant" },
            new TenantExternalProvider
            {
                TenantId = tenantId,
                Provider = "wechat_miniapp",
                Capability = TenantExternalProviderCapability.Identity,
                Status = TenantExternalProviderStatus.Active,
                SecretRef = secretRef,
                ConfigPublic = JsonSerializer.SerializeToElement(new { appId = "wx-app-id" })
            },
            new TenantSecret
            {
                TenantId = tenantId,
                Purpose = "identity",
                Provider = "wechat_miniapp",
                SecretKey = "default",
                SecretRef = secretRef,
                Status = TenantSecretStatus.Active,
                EncryptionKeyId = protectedSecret.KeyId,
                EncryptedPayload = protectedSecret.Ciphertext,
                EncryptionNonce = protectedSecret.Nonce,
                EncryptionTag = protectedSecret.Tag
            }
        };
        entities.AddRange(additionalEntities);
        await factory.SeedAsync(entities.ToArray());
    }

    private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLoginUserAsync(
        ApiTestFactory factory)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var phone = "13800000000";
        await factory.SeedAsync(
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Test Tenant"
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Test User"
            }.WithTestPassword(),
            new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.TenantAdmin,
                Status = MembershipStatus.Active
            });

        return (tenantId, userId, phone);
    }

    private static async Task SeedSmsCodeAsync(
        ApiTestFactory factory,
        Guid tenantId,
        string phone,
        string code)
    {
        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        var smsOptions = scope.ServiceProvider.GetRequiredService<IOptions<SmsSecurityOptions>>().Value;
        dbContext.SmsVerificationCodes.Add(new SmsVerificationCode
        {
            TenantId = tenantId,
            Phone = phone,
            Purpose = SmsPurpose.Login,
            CodeHash = SmsCodeHashing.Hash(
                tenantId,
                phone,
                SmsPurpose.Login,
                code,
                smsOptions.CodePepper),
            Status = SmsVerificationStatus.Sent,
            ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5)
        });
        await dbContext.SaveChangesAsync();
    }

    private static async Task<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
    {
        var stream = await response.Content.ReadAsStreamAsync();
        return await JsonDocument.ParseAsync(stream);
    }

    private static ProtectedTenantSecret ProtectTenantSecret(Guid tenantId, string secretRef, JsonElement payload)
    {
        var protector = new TenantSecretProtector(Options.Create(new TenantSecretEncryptionOptions
        {
            KeyId = "development-v1",
            MasterKey = TenantSecretEncryptionOptions.DevelopmentMasterKey
        }));
        return protector.Protect(tenantId, secretRef, payload);
    }

    private sealed class FakeWechatOAuthClient : IWechatOAuthClient
    {
        public Task<WechatIdentity> ExchangeWebCodeAsync(
            WechatProviderOptions options,
            string code,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new WechatIdentity(
                "web-open-id",
                "union-id",
                "Wechat User",
                "https://example.test/avatar.png",
                null,
                """{"openid":"web-open-id","unionid":"union-id"}"""));
        }

        public Task<WechatIdentity> ExchangeMiniAppCodeAsync(
            WechatProviderOptions options,
            string code,
            CancellationToken cancellationToken = default)
        {
            return Task.FromResult(new WechatIdentity(
                "mini-open-id",
                "union-id",
                null,
                null,
                "session-key",
                """{"openid":"mini-open-id","unionid":"union-id","session_key":"session-key"}"""));
        }
    }

    private sealed class CapturingSmsProvider : ISmsProvider
    {
        public int SendCount { get; private set; }
        public string? Code { get; private set; }

        public Task<SmsProviderSendResult> SendAsync(
            SmsProviderSendRequest request,
            CancellationToken cancellationToken = default)
        {
            SendCount++;
            Code = request.Code;
            return Task.FromResult(new SmsProviderSendResult("test", "sent", "sms-message-id"));
        }
    }
}