using CodeRemoteManager.SetupApp;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace CodeRemoteManager.SetupApp.Tests;

[TestClass]
public sealed class TailscaleExposureGuardTests
{
    [TestMethod]
    public void AllowsThePwaAndUnrelatedTailnetServices()
    {
        TailscaleExposurePolicy.Validate(
            """
            {
              "TCP": { "443": { "HTTPS": true } },
              "AllowFunnel": { "device.example.ts.net:443": false },
              "Web": {
                "device.example.ts.net:443": {
                  "Handlers": {
                    "/": { "Proxy": "http://127.0.0.1:43111" },
                    "/other": { "Proxy": "http://127.0.0.1:45000" }
                  }
                }
              }
            }
            """,
            """{ "version": "0.0.1" }""",
            43110);
    }

    [TestMethod]
    public void RejectsBackendWebAndForegroundTcpMappings()
    {
        var webError = Assert.ThrowsException<TailscaleExposureException>(() =>
            TailscaleExposurePolicy.Validate(
                """{ "Web": { "device:443": { "Handlers": { "/": { "Proxy": "http://127.0.0.1:43110" } } } } }""",
                """{ "version": "0.0.1" }""",
                43110));
        Assert.AreEqual("tailscale_backend_exposed", webError.Message);

        var tcpError = Assert.ThrowsException<TailscaleExposureException>(() =>
            TailscaleExposurePolicy.Validate(
                """{ "Foreground": { "session": { "TCP": { "8443": { "TCPForward": "127.0.0.1:43110" } } } } }""",
                """{ "version": "0.0.1" }""",
                43110));
        Assert.AreEqual("tailscale_backend_exposed", tcpError.Message);
    }

    [TestMethod]
    public void RejectsBackendMappingsInTailscaleServices()
    {
        var error = Assert.ThrowsException<TailscaleExposureException>(() =>
            TailscaleExposurePolicy.Validate(
                "{}",
                """
                {
                  "version": "0.0.1",
                  "services": {
                    "svc:private": {
                      "endpoints": { "tcp:443": "http://127.0.0.1:43110" }
                    }
                  }
                }
                """,
                43110));

        Assert.AreEqual("tailscale_backend_exposed", error.Message);
    }

    [TestMethod]
    public void RejectsFunnelInBackgroundOrForegroundConfiguration()
    {
        foreach (var status in new[]
        {
            """{ "AllowFunnel": { "device:443": true } }""",
            """{ "Foreground": { "session": { "AllowFunnel": { "device:443": true } } } }"""
        })
        {
            var error = Assert.ThrowsException<TailscaleExposureException>(() =>
                TailscaleExposurePolicy.Validate(status, """{ "version": "0.0.1" }""", 43110));
            Assert.AreEqual("tailscale_funnel_exposed", error.Message);
        }
    }

    [TestMethod]
    public void FailsClosedWhenEitherReadOnlyStatusIsInvalid()
    {
        foreach (var pair in new[]
        {
            (Status: "not-json", Services: "{}"),
            (Status: "{}", Services: "not-json")
        })
        {
            var error = Assert.ThrowsException<TailscaleExposureException>(() =>
                TailscaleExposurePolicy.Validate(pair.Status, pair.Services, 43110));
            Assert.AreEqual("tailscale_exposure_status_invalid", error.Message);
        }
    }

    [TestMethod]
    public void GuardReadsDeviceAndServiceConfigurationWithoutMutatingTailscale()
    {
        var commands = new List<string>();
        var guard = new TailscaleExposureGuard("tailscale.exe", (_, arguments) =>
        {
            commands.Add(arguments);
            return arguments == "serve status --json" ? "{}" : """{ "version": "0.0.1" }""";
        });

        guard.EnsureSafe(43110);

        CollectionAssert.AreEqual(
            new[] { "serve status --json", "serve get-config --all" },
            commands);
    }

    [TestMethod]
    public void GuardTurnsCommandFailureIntoASafeUnavailableError()
    {
        var guard = new TailscaleExposureGuard("tailscale.exe", (_, _) => throw new IOException());

        var error = Assert.ThrowsException<TailscaleExposureException>(() => guard.EnsureSafe(43110));

        Assert.AreEqual("tailscale_exposure_status_unavailable", error.Message);
    }
}
