using System.Net;
using System.Text;
using System.Text.Json;

using Microsoft.AspNetCore.Http;
using Microsoft.Teams.Api;
using Microsoft.Teams.Api.Activities;
using Microsoft.Teams.Api.Auth;
using Microsoft.Teams.Apps;
using Microsoft.Teams.Apps.Events;
using Microsoft.Teams.Common.Logging;

using Moq;

namespace Microsoft.Teams.Plugins.AspNetCore.Tests;

public class AspNetCorePluginTests
{
    private static AspNetCorePlugin CreatePlugin(Mock<ILogger>? loggerMock = null, EventFunction? events = null)
    {
        var plugin = new AspNetCorePlugin();
        if (loggerMock is not null)
        {
            plugin.Logger = loggerMock.Object;
        }
        else
        {
            plugin.Logger = new ConsoleLogger("Test", LogLevel.Debug);
        }
        plugin.Client = new Mock<Microsoft.Teams.Common.Http.IHttpClient>().Object;
        if (events is not null)
        {
            plugin.Events += events;
        }
        return plugin;
    }

    private static DefaultHttpContext CreateHttpContext(IActivity activity, string bearer = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.signature")
    {
        var ctx = new DefaultHttpContext();
        ctx.TraceIdentifier = Guid.NewGuid().ToString();
        ctx.Request.Headers.Append("Authorization", $"Bearer {bearer}");
        var json = JsonSerializer.Serialize(activity, new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
        var bytes = Encoding.UTF8.GetBytes(json);
        ctx.Request.Body = new MemoryStream(bytes);
        ctx.Request.ContentLength = bytes.Length;
        return ctx;
    }

    private static MessageActivity CreateMessageActivity(string? serviceUrl = null)
    {
        return new MessageActivity("hi")
        {
            From = new() { Id = "user" },
            Recipient = new() { Id = "bot" },
            Conversation = new Conversation() { Id = "conv", Type = ConversationType.Personal },
            ServiceUrl = serviceUrl
        };
    }

    // Builds an unsigned-but-parseable JWT carrying the given serviceurl claim (omitted when null).
    // JsonWebToken/JwtSecurityTokenHandler.ReadJwtToken parses without verifying the signature.
    private static string CreateJwt(string? serviceUrl)
    {
        var payload = new Dictionary<string, object> { ["exp"] = 4702515200L };
        if (serviceUrl is not null)
        {
            payload["serviceurl"] = serviceUrl;
        }
        return $"{Base64Url(new { alg = "HS256", typ = "JWT" })}.{Base64Url(payload)}.signature";
    }

    private static string Base64Url(object value) =>
        Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value)))
            .TrimEnd('=').Replace('+', '-').Replace('/', '_');

    [Fact]
    public async Task Test_Do_Http_CallsExtractTokenAndActivity_AndCallsCoreDo()
    {
        // Arrange
        var activity = CreateMessageActivity();
        var coreResponse = new Response(HttpStatusCode.Accepted, new { ok = true });
        var eventsCalled = new List<string>();

        EventFunction events = (plugin, name, payload, ct) =>
        {
            eventsCalled.Add(name);
            if (name == "activity") return Task.FromResult<object?>(coreResponse); // returned directly by core Do
            return Task.FromResult<object?>(null);
        };

        var logger = new Mock<ILogger>();
        var plugin = CreatePlugin(logger, events);
        var ctx = CreateHttpContext(activity);

        // Act
        var result = await plugin.Do(ctx);

        // Assert
        Assert.Contains("activity", eventsCalled);
        var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
        Assert.Equal((int)coreResponse.Status, jsonResult.StatusCode);
    }

    [Fact]
    public async Task Test_Do_Http_SetsHeadersFromResponseMeta()
    {
        // Arrange
        var activity = CreateMessageActivity();
        var response = new Response(HttpStatusCode.OK, new { hello = "world" });
        response.Meta.Add("routes", 3);
        response.Meta.Add("custom", "value");

        EventFunction events = (plugin, name, payload, ct) =>
        {
            if (name == "activity") return Task.FromResult<object?>(response);
            return Task.FromResult<object?>(null);
        };

        var plugin = CreatePlugin(new Mock<ILogger>(), events);
        var ctx = CreateHttpContext(activity);

        // Act
        var result = await plugin.Do(ctx);

        // Assert body result type
        var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
        Assert.Equal((int)HttpStatusCode.OK, jsonResult.StatusCode);
        // Headers: routes & custom should be present
        Assert.Contains("X-Teams-Routes", ctx.Response.Headers.Keys); // capitalized first char
        Assert.Contains("X-Teams-Custom", ctx.Response.Headers.Keys);
    }

    [Fact]
    public async Task Test_Do_Http_ErrorPath_ProducesProblemResult()
    {
        // Arrange -> throw inside events
        EventFunction events = (plugin, name, payload, ct) =>
        {
            if (name == "activity") throw new InvalidOperationException("boom");
            return Task.FromResult<object?>(null);
        };

        var logger = new Mock<ILogger>();
        var plugin = CreatePlugin(logger, events);
        var ctx = CreateHttpContext(CreateMessageActivity());

        // Act
        var result = await plugin.Do(ctx);

        // Assert
        var problem = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object>>(result);
        Assert.Equal(500, problem.StatusCode);
        Assert.Contains("boom", problem.Value!.ToString());
        logger.Verify(l => l.Error(It.IsAny<object[]>()), Times.AtLeastOnce);
    }

    [Fact]
    public void Test_ExtractToken_ReturnsToken()
    {
        var plugin = CreatePlugin();
        var ctx = CreateHttpContext(CreateMessageActivity(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.token123");

        var token = plugin.ExtractToken(ctx.Request);
        Assert.NotNull(token);
        Assert.Contains("token123", token.ToString());
    }

    [Fact]
    public async Task Test_ExtractActivity_ReturnsActivity()
    {
        var plugin = CreatePlugin();
        var activity = CreateMessageActivity();
        var ctx = CreateHttpContext(activity);

        var extracted = await plugin.ParseActivity(ctx.Request);
        Assert.NotNull(extracted);
        Assert.True(activity.Type.Equals(extracted.Type));
    }

    [Fact]
    public async Task Test_ExtractActivity_HttpRequestBodyAlreadyRead_ReturnsActivity()
    {
        var plugin = CreatePlugin();
        var activity = CreateMessageActivity();
        var ctx = CreateHttpContext(activity);
        // simulate body already read by setting position to end
        ctx.Request.Body.Position = ctx.Request.Body.Length;

        var extracted = await plugin.ParseActivity(ctx.Request);
        Assert.NotNull(extracted);
        Assert.True(activity.Type.Equals(extracted.Type));
    }

    [Fact]
    public async Task Test_Do_Core_ReturnsResponseAndLogs()
    {
        // Arrange core path tests the ActivityEvent Do(ActivityEvent)
        var response = new Response(HttpStatusCode.OK, new { test = 1 });
        EventFunction events = (plugin, name, payload, ct) =>
        {
            if (name == "activity") return Task.FromResult<object?>(response);
            return Task.FromResult<object?>(null);
        };
        var logger = new Mock<ILogger>();
        var plugin = CreatePlugin(logger, events);
        var evt = new ActivityEvent() { Token = new JsonWebToken("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjQ3MDI1MTUyMDB9.signature"), Activity = CreateMessageActivity() };

        // Act
        var res = await plugin.Do(evt);

        // Assert
        Assert.Same(response, res);
        logger.Verify(l => l.Debug(It.IsAny<object[]>()), Times.AtLeastOnce);
    }

    [Theory]
    [InlineData("https://smba.trafficmanager.net/teams/", "https://smba.trafficmanager.net/teams/")] // exact match
    [InlineData("HTTPS://SMBA.TRAFFICMANAGER.NET/teams/", "https://smba.trafficmanager.net/teams/")] // case-insensitive
    [InlineData("https://smba.trafficmanager.net/teams", "https://smba.trafficmanager.net/teams/")] // trailing-slash normalized
    public async Task Test_Do_Http_ServiceUrlClaimMatches_Processes(string claimServiceUrl, string activityServiceUrl)
    {
        // Arrange
        var activity = CreateMessageActivity(activityServiceUrl);
        var coreResponse = new Response(HttpStatusCode.Accepted, new { ok = true });
        var eventsCalled = new List<string>();
        EventFunction events = (plugin, name, payload, ct) =>
        {
            eventsCalled.Add(name);
            if (name == "activity") return Task.FromResult<object?>(coreResponse);
            return Task.FromResult<object?>(null);
        };
        var plugin = CreatePlugin(new Mock<ILogger>(), events);
        var ctx = CreateHttpContext(activity, CreateJwt(claimServiceUrl));

        // Act
        var result = await plugin.Do(ctx);

        // Assert
        Assert.Contains("activity", eventsCalled);
        var jsonResult = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.JsonHttpResult<object?>>(result);
        Assert.Equal((int)coreResponse.Status, jsonResult.StatusCode);
    }

    [Fact]
    public async Task Test_Do_Http_ServiceUrlClaimMismatch_ReturnsUnauthorized()
    {
        // Arrange
        var activity = CreateMessageActivity("https://smba.trafficmanager.net/teams/");
        var eventsCalled = new List<string>();
        EventFunction events = (plugin, name, payload, ct) =>
        {
            eventsCalled.Add(name);
            if (name == "activity") return Task.FromResult<object?>(new Response(HttpStatusCode.OK, new { ok = true }));
            return Task.FromResult<object?>(null);
        };
        var logger = new Mock<ILogger>();
        var plugin = CreatePlugin(logger, events);
        var ctx = CreateHttpContext(activity, CreateJwt("https://evil.example.com/"));

        // Act
        var result = await plugin.Do(ctx);

        // Assert: rejected with 401, activity never dispatched, neither URL echoed, logged server-side.
        var unauthorized = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult>(result);
        Assert.Equal(401, unauthorized.StatusCode);
        Assert.DoesNotContain("activity", eventsCalled);
        logger.Verify(l => l.Warn(It.IsAny<object[]>()), Times.Once);
    }

    [Fact]
    public async Task Test_Do_Http_NoServiceUrlClaim_ReturnsUnauthorized()
    {
        // Arrange: activity carries a serviceUrl but the token has no serviceurl claim.
        var activity = CreateMessageActivity("https://smba.trafficmanager.net/teams/");
        var eventsCalled = new List<string>();
        EventFunction events = (plugin, name, payload, ct) =>
        {
            eventsCalled.Add(name);
            if (name == "activity") return Task.FromResult<object?>(new Response(HttpStatusCode.OK, new { ok = true }));
            return Task.FromResult<object?>(null);
        };
        var logger = new Mock<ILogger>();
        var plugin = CreatePlugin(logger, events);
        var ctx = CreateHttpContext(activity, CreateJwt(null));

        // Act
        var result = await plugin.Do(ctx);

        // Assert: a token without a serviceurl claim is rejected when the activity has one.
        var unauthorized = Assert.IsType<Microsoft.AspNetCore.Http.HttpResults.UnauthorizedHttpResult>(result);
        Assert.Equal(401, unauthorized.StatusCode);
        Assert.DoesNotContain("activity", eventsCalled);
        logger.Verify(l => l.Warn(It.IsAny<object[]>()), Times.Once);
    }
}