using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Communication;

namespace ScadaLink.InboundAPI.Tests;

/// <summary>
/// WP-3: Tests for script execution on central — timeout, handler dispatch, error handling.
/// WP-5: Safe error messages.
/// </summary>
public class InboundScriptExecutorTests
{
    private readonly InboundScriptExecutor _executor;
    private readonly RouteHelper _route;

    public InboundScriptExecutorTests()
    {
        _executor = new InboundScriptExecutor(NullLogger<InboundScriptExecutor>.Instance, Substitute.For<IServiceProvider>());
        var locator = Substitute.For<IInstanceLocator>();
        var commService = Substitute.For<CommunicationService>(
            Microsoft.Extensions.Options.Options.Create(new CommunicationOptions()),
            NullLogger<CommunicationService>.Instance);
        _route = new RouteHelper(locator, commService);
    }

    [Fact]
    public async Task RegisteredHandler_ExecutesSuccessfully()
    {
        var method = new ApiMethod("test", "return 42;") { Id = 1, TimeoutSeconds = 10 };
        _executor.RegisterHandler("test", async ctx =>
        {
            await Task.CompletedTask;
            return new { result = 42 };
        });

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromSeconds(10));

        Assert.True(result.Success);
        Assert.NotNull(result.ResultJson);
        Assert.Contains("42", result.ResultJson);
    }

    [Fact]
    public async Task UnregisteredHandler_InvalidScript_ReturnsCompilationFailure()
    {
        // Use an invalid script that cannot be compiled by Roslyn
        var method = new ApiMethod("unknown", "%%% invalid C# %%%") { Id = 1, TimeoutSeconds = 10 };

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromSeconds(10));

        Assert.False(result.Success);
        Assert.Contains("Script compilation failed", result.ErrorMessage);
    }

    [Fact]
    public async Task UnregisteredHandler_ValidScript_LazyCompiles()
    {
        // Valid script that is not pre-registered triggers lazy compilation
        var method = new ApiMethod("lazy", "return 1;") { Id = 1, TimeoutSeconds = 10 };

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromSeconds(10));

        Assert.True(result.Success);
    }

    [Fact]
    public async Task HandlerThrows_ReturnsSafeErrorMessage()
    {
        var method = new ApiMethod("failing", "throw new Exception();") { Id = 1, TimeoutSeconds = 10 };
        _executor.RegisterHandler("failing", _ => throw new InvalidOperationException("internal detail leak"));

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromSeconds(10));

        Assert.False(result.Success);
        // WP-5: Safe error message — should NOT contain "internal detail leak"
        Assert.Equal("Internal script error", result.ErrorMessage);
    }

    [Fact]
    public async Task HandlerTimesOut_ReturnsTimeoutError()
    {
        var method = new ApiMethod("slow", "Thread.Sleep(60000);") { Id = 1, TimeoutSeconds = 1 };
        _executor.RegisterHandler("slow", async ctx =>
        {
            await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken);
            return "never";
        });

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromMilliseconds(100));

        Assert.False(result.Success);
        Assert.Contains("timed out", result.ErrorMessage);
    }

    [Fact]
    public async Task HandlerAccessesParameters()
    {
        var method = new ApiMethod("echo", "return params;") { Id = 1, TimeoutSeconds = 10 };
        _executor.RegisterHandler("echo", async ctx =>
        {
            await Task.CompletedTask;
            return ctx.Parameters["name"];
        });

        var parameters = new Dictionary<string, object?> { { "name", "ScadaLink" } };

        var result = await _executor.ExecuteAsync(
            method, parameters, _route, TimeSpan.FromSeconds(10));

        Assert.True(result.Success);
        Assert.Contains("ScadaLink", result.ResultJson!);
    }

    // --- InboundAPI-001: concurrent lazy-compile must not corrupt the handler cache ---

    [Fact]
    public async Task ConcurrentLazyCompile_SameMethod_DoesNotCorruptCache()
    {
        // Many concurrent first-callers of an uncompiled method race the lazy-compile
        // path. With an unsynchronized Dictionary this can throw or return a torn/null
        // handler; all calls must succeed and produce the same result.
        var method = new ApiMethod("concurrent", "return 7;") { Id = 1, TimeoutSeconds = 10 };

        var tasks = Enumerable.Range(0, 64).Select(_ => Task.Run(() =>
            _executor.ExecuteAsync(
                method,
                new Dictionary<string, object?>(),
                _route,
                TimeSpan.FromSeconds(10)))).ToArray();

        var results = await Task.WhenAll(tasks);

        Assert.All(results, r =>
        {
            Assert.True(r.Success, r.ErrorMessage);
            Assert.Equal("7", r.ResultJson);
        });
    }

    [Fact]
    public async Task ConcurrentRegisterAndExecute_DoesNotThrow()
    {
        // RegisterHandler/RemoveHandler racing ExecuteAsync must not crash the process
        // with an InvalidOperationException from concurrent Dictionary mutation.
        var method = new ApiMethod("racy", "return 1;") { Id = 1, TimeoutSeconds = 10 };

        var writers = Enumerable.Range(0, 32).Select(i => Task.Run(() =>
        {
            for (var n = 0; n < 50; n++)
            {
                _executor.RegisterHandler("racy", async ctx => { await Task.CompletedTask; return i; });
                _executor.RemoveHandler("racy");
            }
        }));

        var readers = Enumerable.Range(0, 32).Select(_ => Task.Run(async () =>
        {
            for (var n = 0; n < 50; n++)
            {
                await _executor.ExecuteAsync(
                    method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
            }
        }));

        // Should complete without an unhandled concurrency exception.
        await Task.WhenAll(writers.Concat(readers));
    }

    // --- InboundAPI-005: compiled scripts must not bypass the script trust model ---

    [Theory]
    [InlineData("System.IO.File.Delete(\"/tmp/x\"); return null;")]
    [InlineData("System.Diagnostics.Process.Start(\"/bin/sh\"); return null;")]
    [InlineData("var t = System.Reflection.Assembly.GetExecutingAssembly(); return null;")]
    [InlineData("new System.Threading.Thread(() => {}).Start(); return null;")]
    [InlineData("var s = new System.Net.Sockets.Socket(System.Net.Sockets.AddressFamily.InterNetwork, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); return null;")]
    public void CompileAndRegister_ForbiddenApi_RejectsScript(string script)
    {
        var method = new ApiMethod("forbidden", script) { Id = 1, TimeoutSeconds = 10 };

        var registered = _executor.CompileAndRegister(method);

        Assert.False(registered);
    }

    [Fact]
    public async Task ExecuteAsync_ForbiddenApiScript_DoesNotRunAndReturnsFailure()
    {
        // A fully-qualified forbidden API call must be rejected at compile/register time
        // so the script never executes.
        var marker = System.IO.Path.Combine(
            System.IO.Path.GetTempPath(), $"scadalink-pwned-{Guid.NewGuid():N}");
        System.IO.File.Delete(marker);

        var method = new ApiMethod("evil", $"System.IO.File.WriteAllText(@\"{marker}\", \"x\"); return 1;")
        {
            Id = 1,
            TimeoutSeconds = 10
        };

        var result = await _executor.ExecuteAsync(
            method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));

        Assert.False(result.Success);
        Assert.False(System.IO.File.Exists(marker));
    }

    [Fact]
    public void CompileAndRegister_PermittedScript_StillRegisters()
    {
        // The trust-model check must not reject legitimate scripts.
        var method = new ApiMethod("ok", "var list = new List<int> { 1, 2, 3 }; return list.Sum();")
        {
            Id = 1,
            TimeoutSeconds = 10
        };

        Assert.True(_executor.CompileAndRegister(method));
    }

    // --- InboundAPI-002: lazy compile-and-fetch must be atomic, never KeyNotFoundException ---

    [Fact]
    public async Task LazyCompile_RacingRemoveHandler_NeverThrowsKeyNotFound()
    {
        // The lazy-compile path must compile-and-fetch atomically: a concurrent
        // RemoveHandler must not be able to turn a first-call into an "Internal
        // script error" (the old check-then-act re-read could throw KeyNotFoundException).
        var method = new ApiMethod("atomic", "return 5;") { Id = 1, TimeoutSeconds = 10 };

        var removers = Enumerable.Range(0, 16).Select(_ => Task.Run(() =>
        {
            for (var n = 0; n < 200; n++)
                _executor.RemoveHandler("atomic");
        }));

        var callers = Enumerable.Range(0, 16).Select(_ => Task.Run(async () =>
        {
            for (var n = 0; n < 50; n++)
            {
                var r = await _executor.ExecuteAsync(
                    method, new Dictionary<string, object?>(), _route, TimeSpan.FromSeconds(10));
                // Result must always be a clean success or a clean compilation
                // failure — never the catch-all "Internal script error".
                Assert.NotEqual("Internal script error", r.ErrorMessage);
            }
        }));

        await Task.WhenAll(removers.Concat(callers));
    }

    // --- InboundAPI-004: a client disconnect must NOT be reported as a script timeout ---

    [Fact]
    public async Task ClientDisconnect_IsNotReportedAsTimeout()
    {
        // When the caller's request token is cancelled (client aborted the request),
        // ExecuteAsync must report a client-cancelled failure, not "Script execution
        // timed out" — that log line is reserved for genuine timeouts.
        var method = new ApiMethod("aborted", "return 1;") { Id = 1, TimeoutSeconds = 30 };
        _executor.RegisterHandler("aborted", async ctx =>
        {
            await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken);
            return "never";
        });

        using var clientAborted = new CancellationTokenSource();
        clientAborted.CancelAfter(TimeSpan.FromMilliseconds(100));

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            // Generous method timeout so the timeout CTS is NOT the cause.
            TimeSpan.FromSeconds(30),
            clientAborted.Token);

        Assert.False(result.Success);
        Assert.DoesNotContain("timed out", result.ErrorMessage ?? string.Empty);
    }

    [Fact]
    public async Task GenuineTimeout_StillReportedAsTimeout()
    {
        // A method that exceeds its timeout with no client abort must still be
        // reported as "timed out" (regression guard for the InboundAPI-004 fix).
        var method = new ApiMethod("genuine", "return 1;") { Id = 1, TimeoutSeconds = 1 };
        _executor.RegisterHandler("genuine", async ctx =>
        {
            await Task.Delay(TimeSpan.FromSeconds(60), ctx.CancellationToken);
            return "never";
        });

        var result = await _executor.ExecuteAsync(
            method,
            new Dictionary<string, object?>(),
            _route,
            TimeSpan.FromMilliseconds(100),
            CancellationToken.None);

        Assert.False(result.Success);
        Assert.Contains("timed out", result.ErrorMessage);
    }
}
