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

namespace Tiku.IntegrationTests.Api;

public sealed class PointsEndpointTests
{
    [Fact]
    public async Task Anonymous_points_request_returns_401()
    {
        await using var factory = new ApiTestFactory();
        using var client = factory.CreateClient();

        var response = await client.GetAsync("/api/points/summary");

        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Fact]
    public async Task Non_member_cannot_access_points()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedLoginUserAsync(factory, includeMembership: false);
        var sessionId = Guid.NewGuid();
        await factory.SeedAsync(new AuthSession
        {
            Id = sessionId,
            TenantId = seed.TenantId,
            UserId = seed.UserId,
            TokenHash = "integration-test-token-hash",
            Provider = "test",
            ExpiresAt = DateTimeOffset.UtcNow.AddHours(1)
        });
        using var client = factory.CreateClient();
        client.DefaultRequestHeaders.Authorization = new(
            "Bearer",
            TestJwtKeys.CreateToken([
                new Claim(TikuClaimTypes.UserId, seed.UserId.ToString()),
                new Claim(TikuClaimTypes.SessionId, sessionId.ToString()),
                new Claim(TikuClaimTypes.TenantId, seed.TenantId.ToString())
            ]));

        var response = await client.GetAsync("/api/points/summary");

        Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
    }

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

        var sourceId = Guid.NewGuid();
        var first = await client.PostAsJsonAsync(
            "/api/points/tasks/claim",
            new ClaimPointTaskDto
            {
                TaskKey = "daily_login",
                SourceType = "daily",
                SourceId = sourceId
            });
        var second = await client.PostAsJsonAsync(
            "/api/points/tasks/claim",
            new ClaimPointTaskDto
            {
                TaskKey = "daily_login",
                SourceType = "daily",
                SourceId = sourceId
            });
        var firstClaim = await first.Content.ReadFromJsonAsync<PointClaimItem>();
        var secondClaim = await second.Content.ReadFromJsonAsync<PointClaimItem>();
        var summary = await (await client.GetAsync("/api/points/summary"))
            .Content
            .ReadFromJsonAsync<PointSummaryItem>();
        var tasks = await (await client.GetAsync("/api/points/tasks"))
            .Content
            .ReadFromJsonAsync<PointList<PointTaskItem>>();

        Assert.Equal(HttpStatusCode.OK, first.StatusCode);
        Assert.Equal(HttpStatusCode.OK, second.StatusCode);
        Assert.Equal(firstClaim!.Id, secondClaim!.Id);
        Assert.Equal(20, summary!.EarnedPoints);
        Assert.Equal(20, summary.BalancePoints);
        Assert.Contains(tasks!.Items, item => item.TaskKey == "daily_login" && !item.CanClaim);
    }

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

        var response = await client.PostAsJsonAsync(
            "/api/points/exchange-orders",
            new CreatePointExchangeOrderDto { ItemId = seed.ExchangeItemId });

        Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
    }

    [Fact]
    public async Task Student_can_exchange_points_for_entitlement()
    {
        await using var factory = new ApiTestFactory();
        var seed = await SeedLoginUserAsync(factory);
        using var client = factory.CreateClient();
        await LoginAsync(client, seed);
        await client.PostAsJsonAsync(
            "/api/points/tasks/claim",
            new ClaimPointTaskDto { TaskKey = "practice_reward", SourceType = "practice", SourceId = Guid.NewGuid() });

        var items = await (await client.GetAsync("/api/points/exchange-items"))
            .Content
            .ReadFromJsonAsync<PointList<PointExchangeItemDto>>();
        var response = await client.PostAsJsonAsync(
            "/api/points/exchange-orders",
            new CreatePointExchangeOrderDto { ItemId = seed.ExchangeItemId });
        var order = await response.Content.ReadFromJsonAsync<PointExchangeOrderItem>();
        var orders = await (await client.GetAsync("/api/points/exchange-orders"))
            .Content
            .ReadFromJsonAsync<PointList<PointExchangeOrderItem>>();
        var summary = await (await client.GetAsync("/api/points/summary"))
            .Content
            .ReadFromJsonAsync<PointSummaryItem>();

        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        Assert.True(items!.Items.Single(item => item.Id == seed.ExchangeItemId).CanExchange);
        Assert.Equal("Completed", order!.Status);
        Assert.Contains(orders!.Items, item => item.Id == order.Id);
        Assert.Equal(100, summary!.EarnedPoints);
        Assert.Equal(30, summary.SpentPoints);
        Assert.Equal(70, summary.BalancePoints);

        using var scope = factory.CreateSystemScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
        Assert.Contains(dbContext.Entitlements, item =>
            item.TenantId == seed.TenantId &&
            item.UserId == seed.UserId &&
            item.SourceType == "point_exchange_order");
    }

    private static async Task<PointSeed> SeedLoginUserAsync(
        ApiTestFactory factory,
        bool includeMembership = true)
    {
        var tenantId = Guid.NewGuid();
        var userId = Guid.NewGuid();
        var exchangeItemId = Guid.NewGuid();
        var phone = "13800000001";
        var entities = new List<object>
        {
            new Tenant
            {
                Id = tenantId,
                Slug = tenantId.ToString("N"),
                Name = "Points Tenant"
            },
            new User
            {
                Id = userId,
                Phone = phone,
                Name = "Points User"
            }.WithTestPassword(),
            new PointActivityTask
            {
                TenantId = tenantId,
                TaskKey = "daily_login",
                Title = "每日登录",
                TaskType = PointActivityTaskType.DailyLogin,
                Points = 20,
                MaxClaimsPerUser = 1,
                SortOrder = 1
            },
            new PointActivityTask
            {
                TenantId = tenantId,
                TaskKey = "practice_reward",
                Title = "练习奖励",
                TaskType = PointActivityTaskType.Practice,
                Points = 100,
                MaxClaimsPerUser = 3,
                SortOrder = 2
            },
            new PointExchangeItem
            {
                Id = exchangeItemId,
                TenantId = tenantId,
                ItemKey = "svip_7d",
                Name = "SVIP 7 天",
                ItemType = PointExchangeItemType.Entitlement,
                PointsCost = 30,
                Days = 7,
                Stock = 5,
                SortOrder = 1
            }
        };
        if (includeMembership)
        {
            entities.Add(new TenantMembership
            {
                TenantId = tenantId,
                UserId = userId,
                Role = TenantRole.Student,
                Status = MembershipStatus.Active
            });
        }

        await factory.SeedAsync(entities.ToArray());
        return new PointSeed(tenantId, userId, exchangeItemId, phone);
    }

    private static async Task LoginAsync(HttpClient client, PointSeed seed)
    {
        client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone));
    }

    private sealed record PointSeed(Guid TenantId, Guid UserId, Guid ExchangeItemId, string Phone);
}
