using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Entities.Notifications;
using ScadaLink.Commons.Interfaces.Repositories;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.InboundAPI;
using ScadaLink.NotificationService;

namespace ScadaLink.IntegrationTests;

/// <summary>
/// WP-14: End-to-end integration tests for Phase 7 integration surfaces.
/// </summary>
public class IntegrationSurfaceTests
{
    // ── Inbound API: auth + routing + parameter validation + error codes ──

    [Fact]
    public async Task InboundAPI_ApiKeyValidator_FullFlow_EndToEnd()
    {
        // Validates that ApiKeyValidator correctly chains all checks.
        var repository = Substitute.For<IInboundApiRepository>();
        var key = new ApiKey("test-key", "key-value-123") { Id = 1, IsEnabled = true };
        var method = new ApiMethod("getStatus", "return 1;")
        {
            Id = 10,
            ParameterDefinitions = "[{\"Name\":\"deviceId\",\"Type\":\"String\",\"Required\":true}]",
            TimeoutSeconds = 30
        };

        repository.GetApiKeyByValueAsync("key-value-123").Returns(key);
        repository.GetMethodByNameAsync("getStatus").Returns(method);
        repository.GetApprovedKeysForMethodAsync(10).Returns(new List<ApiKey> { key });

        var validator = new ApiKeyValidator(repository);

        // Valid key + approved method
        var result = await validator.ValidateAsync("key-value-123", "getStatus");
        Assert.True(result.IsValid);
        Assert.Equal(method, result.Method);

        // Then validate parameters
        using var doc = JsonDocument.Parse("{\"deviceId\": \"pump-01\"}");
        var paramResult = ParameterValidator.Validate(doc.RootElement.Clone(), method.ParameterDefinitions);
        Assert.True(paramResult.IsValid);
        Assert.Equal("pump-01", paramResult.Parameters["deviceId"]);
    }

    [Fact]
    public void InboundAPI_ParameterValidation_ExtendedTypes()
    {
        // Validates the full extended type system: Boolean, Integer, Float, String, Object, List.
        var definitions = JsonSerializer.Serialize(new[]
        {
            new { Name = "flag", Type = "Boolean", Required = true },
            new { Name = "count", Type = "Integer", Required = true },
            new { Name = "ratio", Type = "Float", Required = true },
            new { Name = "name", Type = "String", Required = true },
            new { Name = "config", Type = "Object", Required = true },
            new { Name = "tags", Type = "List", Required = true }
        });

        var json = "{\"flag\":true,\"count\":42,\"ratio\":3.14,\"name\":\"test\",\"config\":{\"k\":\"v\"},\"tags\":[1,2]}";
        using var doc = JsonDocument.Parse(json);
        var result = ParameterValidator.Validate(doc.RootElement.Clone(), definitions);

        Assert.True(result.IsValid);
        Assert.Equal(true, result.Parameters["flag"]);
        Assert.Equal((long)42, result.Parameters["count"]);
        Assert.Equal(3.14, result.Parameters["ratio"]);
        Assert.Equal("test", result.Parameters["name"]);
        Assert.NotNull(result.Parameters["config"]);
        Assert.NotNull(result.Parameters["tags"]);
    }

    // ── External System: error classification ──

    [Fact]
    public void ExternalSystem_ErrorClassification_TransientVsPermanent()
    {
        // WP-8: Verify the full classification spectrum
        Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.InternalServerError));
        Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.ServiceUnavailable));
        Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.RequestTimeout));
        Assert.True(ExternalSystemGateway.ErrorClassifier.IsTransient((HttpStatusCode)429));

        Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.BadRequest));
        Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.Unauthorized));
        Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.Forbidden));
        Assert.False(ExternalSystemGateway.ErrorClassifier.IsTransient(HttpStatusCode.NotFound));
    }

    // ── Notification: mock SMTP delivery ──

    [Fact]
    public async Task Notification_Send_MockSmtp_Delivers()
    {
        var repository = Substitute.For<INotificationRepository>();
        var smtpClient = Substitute.For<ISmtpClientWrapper>();

        var list = new NotificationList("alerts") { Id = 1 };
        var recipients = new List<NotificationRecipient>
        {
            new("Admin", "admin@example.com") { Id = 1, NotificationListId = 1 }
        };
        var smtpConfig = new SmtpConfiguration("smtp.example.com", "basic", "noreply@example.com")
        {
            Id = 1, Port = 587, Credentials = "user:pass"
        };

        repository.GetListByNameAsync("alerts").Returns(list);
        repository.GetRecipientsByListIdAsync(1).Returns(recipients);
        repository.GetAllSmtpConfigurationsAsync().Returns(new List<SmtpConfiguration> { smtpConfig });

        var service = new NotificationDeliveryService(
            repository,
            () => smtpClient,
            Microsoft.Extensions.Logging.Abstractions.NullLogger<NotificationDeliveryService>.Instance);

        var result = await service.SendAsync("alerts", "Test Alert", "Something happened");

        Assert.True(result.Success);
        await smtpClient.Received(1).SendAsync(
            "noreply@example.com",
            Arg.Is<IEnumerable<string>>(r => r.Contains("admin@example.com")),
            "Test Alert",
            "Something happened",
            Arg.Any<CancellationToken>());
    }

    // ── Script Context: integration API wiring ──

    [Fact]
    public async Task ScriptContext_ExternalSystem_Call_Wired()
    {
        // Verify that ExternalSystem.Call is accessible from ScriptRuntimeContext
        var mockClient = Substitute.For<IExternalSystemClient>();
        mockClient.CallAsync("api", "getData", null, Arg.Any<CancellationToken>())
            .Returns(new ExternalCallResult(true, "{\"value\":1}", null));

        var context = CreateMinimalScriptContext(externalSystemClient: mockClient);

        var result = await context.ExternalSystem.Call("api", "getData");

        Assert.True(result.Success);
        Assert.Equal("{\"value\":1}", result.ResponseJson);
    }

    [Fact]
    public async Task ScriptContext_Notify_Send_Wired()
    {
        var mockNotify = Substitute.For<INotificationDeliveryService>();
        mockNotify.SendAsync("ops", "Alert", "Body", Arg.Any<string?>(), Arg.Any<CancellationToken>())
            .Returns(new NotificationResult(true, null));

        var context = CreateMinimalScriptContext(notificationService: mockNotify);

        var result = await context.Notify.To("ops").Send("Alert", "Body");

        Assert.True(result.Success);
    }

    [Fact]
    public async Task ScriptContext_ExternalSystem_NoClient_Throws()
    {
        var context = CreateMinimalScriptContext();

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.ExternalSystem.Call("api", "method"));
    }

    [Fact]
    public async Task ScriptContext_Database_NoGateway_Throws()
    {
        var context = CreateMinimalScriptContext();

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.Database.Connection("db"));
    }

    [Fact]
    public async Task ScriptContext_Notify_NoService_Throws()
    {
        var context = CreateMinimalScriptContext();

        await Assert.ThrowsAsync<InvalidOperationException>(
            () => context.Notify.To("list").Send("subj", "body"));
    }

    private static SiteRuntime.Scripts.ScriptRuntimeContext CreateMinimalScriptContext(
        IExternalSystemClient? externalSystemClient = null,
        IDatabaseGateway? databaseGateway = null,
        INotificationDeliveryService? notificationService = null)
    {
        // Create a minimal context — we use Substitute.For<IActorRef> which is fine since
        // we won't exercise Akka functionality in these tests.
        var actorRef = Substitute.For<Akka.Actor.IActorRef>();
        var compilationService = new SiteRuntime.Scripts.ScriptCompilationService(
            Microsoft.Extensions.Logging.Abstractions.NullLogger<SiteRuntime.Scripts.ScriptCompilationService>.Instance);
        var sharedLibrary = new SiteRuntime.Scripts.SharedScriptLibrary(
            compilationService,
            Microsoft.Extensions.Logging.Abstractions.NullLogger<SiteRuntime.Scripts.SharedScriptLibrary>.Instance);

        return new SiteRuntime.Scripts.ScriptRuntimeContext(
            actorRef,
            actorRef,
            sharedLibrary,
            currentCallDepth: 0,
            maxCallDepth: 10,
            askTimeout: TimeSpan.FromSeconds(5),
            instanceName: "test-instance",
            logger: Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance,
            externalSystemClient: externalSystemClient,
            databaseGateway: databaseGateway,
            notificationService: notificationService);
    }
}
