"""Unit: bundle bringup primitives for the smolmachines backend
(PRD 0023 chunk 2c).

Tests mock `subprocess.run` and assert on the docker argv shape.
The end-to-end integration smoke (real docker daemon, real
bundle image) lands in chunk 2d."""

from __future__ import annotations

import subprocess
import unittest
from pathlib import Path
from unittest.mock import patch

from bot_bottle.backend.smolmachines.sidecar_bundle import (
    BundleLaunchSpec,
    bundle_container_name,
    bundle_network_name,
    create_bundle_network,
    ensure_bundle_image,
    remove_bundle_network,
    start_bundle,
    stop_bundle,
)


def _ok(stdout: str = "", stderr: str = "") -> subprocess.CompletedProcess:  # type: ignore
    return subprocess.CompletedProcess(
        args=[], returncode=0, stdout=stdout, stderr=stderr,
    )


def _fail(stderr: str = "boom") -> subprocess.CompletedProcess:  # type: ignore
    return subprocess.CompletedProcess(
        args=[], returncode=1, stdout="", stderr=stderr,
    )


def _spec(**kwargs) -> BundleLaunchSpec:  # type: ignore
    defaults = dict(
        slug="demo-abc12",
        network_name="bot-bottle-bundle-demo-abc12",
        subnet="192.168.50.0/24",
        gateway="192.168.50.1",
        bundle_ip="192.168.50.2",
    )
    defaults.update(kwargs)
    return BundleLaunchSpec(**defaults)  # type: ignore


class TestNamingHelpers(unittest.TestCase):
    def test_network_name_uses_bundle_prefix(self):
        # Distinct from the docker backend's
        # `bot-bottle-net-<slug>` so two backends running the
        # same agent slug don't collide.
        self.assertEqual(
            "bot-bottle-bundle-myagent-xyz",
            bundle_network_name("myagent-xyz"),
        )

    def test_container_name_matches_docker_bundle_shape(self):
        # Same shape PRD 0024 chunk 5 set for the docker backend's
        # bundle container — dashboard prefix-discovery covers
        # both backends with one filter.
        self.assertEqual(
            "bot-bottle-sidecars-myagent-xyz",
            bundle_container_name("myagent-xyz"),
        )


class TestNetworkLifecycle(unittest.TestCase):
    def _patch_run(self, **kwargs):  # type: ignore
        return patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
            **kwargs,
        )

    def test_create_argv_explicit_subnet_and_gateway(self):
        with self._patch_run(return_value=_ok()) as m:
            create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1")
        self.assertEqual(
            ["docker", "network", "create",
             "--subnet", "192.168.50.0/24",
             "--gateway", "192.168.50.1",
             "nn"],
            m.call_args.args[0],
        )

    def test_create_treats_existing_network_as_success(self):
        with self._patch_run(return_value=_fail("network nn already exists")):
            # No SystemExit.
            create_bundle_network("nn", "192.168.50.0/24", "192.168.50.1")

    def test_create_other_failure_is_fatal(self):
        with self._patch_run(return_value=_fail("invalid subnet")):
            with self.assertRaises(SystemExit):
                create_bundle_network("nn", "bogus", "bogus")

    def test_remove_missing_network_is_idempotent(self):
        # No SystemExit / no warn-and-continue noise; missing
        # network is the expected case during a partial teardown.
        with self._patch_run(return_value=_fail("Error: No such network: nn")):
            remove_bundle_network("nn")

    def test_remove_clean_returns_success(self):
        with self._patch_run(return_value=_ok()):
            remove_bundle_network("nn")


class TestStartBundle(unittest.TestCase):
    def _patch_run(self):
        return patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
            return_value=_ok(),
        )

    def test_argv_pins_ip_on_network(self):
        with self._patch_run() as m:
            start_bundle(_spec())
        argv = m.call_args.args[0]
        # --network NETNAME --ip <bundle-ip> on the docker run.
        self.assertIn("--network", argv)
        self.assertIn("bot-bottle-bundle-demo-abc12", argv)
        self.assertIn("--ip", argv)
        self.assertIn("192.168.50.2", argv)
        # Detached and auto-removed.
        self.assertIn("--detach", argv)
        self.assertIn("--rm", argv)
        # Container name uses the per-slug bundle prefix.
        i = argv.index("--name")
        self.assertEqual("bot-bottle-sidecars-demo-abc12", argv[i + 1])
        # Image at the end.
        self.assertEqual("bot-bottle-sidecars:latest", argv[-1])

    def test_daemons_env_passed_in(self):
        with self._patch_run() as m:
            start_bundle(_spec(daemons_csv="egress,supervise"))
        argv = m.call_args.args[0]
        self.assertIn("-e", argv)
        self.assertIn(
            "BOT_BOTTLE_SIDECAR_DAEMONS=egress,supervise",
            argv,
        )

    def test_environment_entries_pass_through(self):
        with self._patch_run() as m:
            start_bundle(_spec(environment=(
                "SUPERVISE_BOTTLE_SLUG=demo-abc12",
                "EGRESS_TOKEN_0",  # bare-name → host env inherit
            )))
        argv = m.call_args.args[0]
        self.assertIn("SUPERVISE_BOTTLE_SLUG=demo-abc12", argv)
        self.assertIn("EGRESS_TOKEN_0", argv)

    def test_volumes_render_with_ro_flag(self):
        with self._patch_run() as m:
            start_bundle(_spec(volumes=(
                ("/host/egress-ca.pem", "/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem", True),
                ("/host/queue", "/run/supervise/queue", False),
            )))
        argv = m.call_args.args[0]
        self.assertIn(
            "/host/egress-ca.pem:/home/mitmproxy/.mitmproxy/mitmproxy-ca.pem:ro",
            argv,
        )
        self.assertIn("/host/queue:/run/supervise/queue", argv)

    def test_failure_dies(self):
        with patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
            return_value=_fail("invalid mount"),
        ):
            with self.assertRaises(SystemExit):
                start_bundle(_spec())

    def test_host_env_inherited_to_subprocess(self):
        # Bare-name entries in spec.environment rely on the docker
        # subprocess being run with the host env. Confirm `env=`
        # threads through.
        with patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
            return_value=_ok(),
        ) as m:
            start_bundle(_spec(), env={"FOO": "bar"})
        self.assertEqual({"FOO": "bar"}, m.call_args.kwargs["env"])


class TestEnsureBundleImage(unittest.TestCase):
    def test_builds_sidecar_dockerfile_before_plain_docker_run(self):
        with patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.docker_mod.build_image",
        ) as build:
            ensure_bundle_image()

        build.assert_called_once()
        args = build.call_args.args
        kwargs = build.call_args.kwargs
        self.assertEqual("bot-bottle-sidecars:latest", args[0])
        self.assertTrue((Path(args[1]) / "Dockerfile.sidecars").is_file())
        self.assertEqual("Dockerfile.sidecars", kwargs["dockerfile"])


class TestStopBundle(unittest.TestCase):
    def _patch_run(self, **kwargs):  # type: ignore
        return patch(
            "bot_bottle.backend.smolmachines.sidecar_bundle.subprocess.run",
            **kwargs,
        )

    def test_argv_force_removes(self):
        with self._patch_run(return_value=_ok()) as m:
            stop_bundle("demo-abc12")
        self.assertEqual(
            ["docker", "rm", "-f", "bot-bottle-sidecars-demo-abc12"],
            m.call_args.args[0],
        )

    def test_missing_container_is_idempotent(self):
        with self._patch_run(return_value=_fail(
            "Error: No such container: bot-bottle-sidecars-demo-abc12"
        )):
            stop_bundle("demo-abc12")  # no raise


if __name__ == "__main__":
    unittest.main()
