using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using JobTrackerApi.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using Moq;
using Xunit;

namespace JobTrackerApi.Tests;

public sealed class MicrosoftTokenValidatorTests
{
    private const string TenantA = "11111111-1111-1111-1111-111111111111";
    private const string TenantB = "22222222-2222-2222-2222-222222222222";
    private const string ObjectId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";

    [Fact]
    public async Task Valid_common_token_returns_the_tenant_object_pair_and_unverified_email_metadata()
    {
        var (validator, key) = BuildHarness("common");

        var result = await validator.ValidateAsync(CreateToken(key, TenantA, ObjectId));

        Assert.Equal(TenantA, result.TenantId);
        Assert.Equal(ObjectId, result.ObjectId);
        Assert.Equal(ObjectId, result.Subject); // Legacy consumer until SEC-004 switches persistence.
        Assert.Equal("demo@example.com", result.Email);
        Assert.False(result.EmailVerified);
    }

    [Fact]
    public void Production_requires_an_explicit_tenant_policy()
    {
        Assert.Throws<InvalidOperationException>(() => MicrosoftTenantPolicy.Parse(null, production: true));
        Assert.Equal("common", MicrosoftTenantPolicy.Parse(null, production: false).Mode);
        Assert.Throws<InvalidOperationException>(() => MicrosoftTenantPolicy.Parse("not-a-tenant", production: false));
    }

    [Theory]
    [InlineData("common", TenantA, true)]
    [InlineData("common", MicrosoftTenantPolicy.ConsumerTenantId, true)]
    [InlineData("organizations", TenantA, true)]
    [InlineData("organizations", MicrosoftTenantPolicy.ConsumerTenantId, false)]
    [InlineData("consumers", MicrosoftTenantPolicy.ConsumerTenantId, true)]
    [InlineData("consumers", TenantA, false)]
    [InlineData(TenantA, TenantA, true)]
    [InlineData(TenantA, TenantB, false)]
    public async Task Tenant_modes_accept_only_their_documented_accounts(string mode, string tokenTenant, bool accepted)
    {
        var (validator, key) = BuildHarness(mode);
        var token = CreateToken(key, tokenTenant, ObjectId);

        if (accepted)
            Assert.Equal(tokenTenant, (await validator.ValidateAsync(token)).TenantId);
        else
            await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(token));
    }

    [Theory]
    [InlineData(null, ObjectId)]
    [InlineData("not-a-guid", ObjectId)]
    [InlineData(TenantA, null)]
    [InlineData(TenantA, "not-a-guid")]
    public async Task Tid_and_oid_are_required_guids(string? tenantId, string? objectId)
    {
        var (validator, key) = BuildHarness("common");

        await Assert.ThrowsAnyAsync<Exception>(() => validator.ValidateAsync(CreateToken(key, tenantId, objectId)));
    }

    [Fact]
    public async Task Issuer_must_exactly_match_the_signed_tid()
    {
        var (validator, key) = BuildHarness("common");
        var token = CreateToken(key, TenantA, ObjectId, issuer: $"https://login.microsoftonline.com/{TenantB}/v2.0");

        await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(token));
    }

    [Fact]
    public async Task Audience_signature_and_lifetime_remain_enforced()
    {
        var (validator, key) = BuildHarness("common");
        var otherKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("different-signing-key-different-signing-key"));

        await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(key, TenantA, ObjectId, audience: "wrong-client")));
        await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(otherKey, TenantA, ObjectId)));
        await Assert.ThrowsAnyAsync<SecurityTokenException>(() => validator.ValidateAsync(CreateToken(key, TenantA, ObjectId, expires: DateTime.UtcNow.AddMinutes(-10))));
    }

    [Fact]
    public async Task Same_oid_in_two_tenants_produces_two_distinct_stable_pairs()
    {
        var (validator, key) = BuildHarness("common");

        var first = await validator.ValidateAsync(CreateToken(key, TenantA, ObjectId));
        var second = await validator.ValidateAsync(CreateToken(key, TenantB, ObjectId));

        Assert.NotEqual(first.TenantId, second.TenantId);
        Assert.Equal(first.ObjectId, second.ObjectId);
    }

    private static (MicrosoftTokenValidator Validator, SymmetricSecurityKey Key) BuildHarness(string tenantMode)
    {
        var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
        {
            ["Auth:MicrosoftClientId"] = "client-123",
            ["Auth:MicrosoftTenant"] = tenantMode,
        }).Build();
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("super-secret-signing-key-super-secret"));
        var oidc = new OpenIdConnectConfiguration();
        oidc.SigningKeys.Add(key);
        var manager = new Mock<IConfigurationManager<OpenIdConnectConfiguration>>();
        manager.Setup(x => x.GetConfigurationAsync(It.IsAny<CancellationToken>())).ReturnsAsync(oidc);
        return (new MicrosoftTokenValidator(config, manager.Object), key);
    }

    private static string CreateToken(
        SecurityKey key,
        string? tenantId,
        string? objectId,
        string? issuer = null,
        string audience = "client-123",
        DateTime? expires = null)
    {
        var claims = new List<Claim>
        {
            new("email", "demo@example.com"),
            new("given_name", "Demo"),
            new("family_name", "User"),
        };
        if (tenantId is not null) claims.Add(new Claim("tid", tenantId));
        if (objectId is not null) claims.Add(new Claim("oid", objectId));
        issuer ??= $"https://login.microsoftonline.com/{tenantId}/v2.0";

        return new JwtSecurityTokenHandler().WriteToken(new JwtSecurityToken(
            issuer,
            audience,
            claims,
            expires: expires ?? DateTime.UtcNow.AddMinutes(10),
            signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)));
    }
}
