"""Tests for network plan executor."""

from unittest.mock import MagicMock, patch

from gaol.networking.archetypes import (
    NetworkdConfig,
    PolicyRoute,
    VethConfig,
    resolve_bridged,
    resolve_host,
    resolve_shared,
    resolve_vpn_exit,
)
from gaol.networking.executor import (
    ExecutionResult,
    StepResult,
    StepStatus,
    add_policy_route,
    apply_plan,
    enable_ip_forward,
    remove_policy_route,
    remove_systemd_nat_masquerade,
    set_route_gateway,
    teardown_plan,
    write_networkd_config,
)


def _mock_run_ok(*args, **kwargs):
    """Mock subprocess.run returning success."""
    m = MagicMock()
    m.returncode = 0
    m.stdout = ""
    m.stderr = ""
    return m


def _mock_run_fail(*args, **kwargs):
    """Mock subprocess.run returning failure."""
    m = MagicMock()
    m.returncode = 1
    m.stdout = ""
    m.stderr = "some error"
    return m


class TestStepResult:
    def test_ok_status(self):
        r = StepResult("test", StepStatus.OK)
        assert r.ok is True

    def test_failed_status(self):
        r = StepResult("test", StepStatus.FAILED, "oops")
        assert r.ok is False

    def test_skipped_status(self):
        r = StepResult("test", StepStatus.SKIPPED)
        assert r.ok is True


class TestExecutionResult:
    def test_all_ok(self):
        result = ExecutionResult(steps=[
            StepResult("a", StepStatus.OK),
            StepResult("b", StepStatus.OK),
        ])
        assert result.ok is True
        assert result.failed_steps == []

    def test_one_failed(self):
        result = ExecutionResult(steps=[
            StepResult("a", StepStatus.OK),
            StepResult("b", StepStatus.FAILED, "err"),
        ])
        assert result.ok is False
        assert len(result.failed_steps) == 1

    def test_summary(self):
        result = ExecutionResult(steps=[
            StepResult("a", StepStatus.OK),
            StepResult("b", StepStatus.FAILED, "err"),
            StepResult("c", StepStatus.SKIPPED),
        ])
        assert result.summary() == "1/3 ok, 1 failed, 1 skipped"


class TestWriteNetworkdConfig:
    @patch("gaol.networking.executor.subprocess.run")
    def test_writes_config(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        config = NetworkdConfig(
            filename="80-container-ve-test.network",
            content="[Match]\nName=ve-test\n",
        )
        result = write_networkd_config(config)
        assert result.status == StepStatus.OK
        # Verify sudo tee was called
        args = mock_run.call_args[0][0]
        assert "tee" in args
        assert "/etc/systemd/network/80-container-ve-test.network" in args

    @patch("gaol.networking.executor.subprocess.run")
    def test_write_failure(self, mock_run):
        mock_run.return_value = _mock_run_fail()
        config = NetworkdConfig(filename="test.network", content="content")
        result = write_networkd_config(config)
        assert result.status == StepStatus.FAILED


class TestAddPolicyRoute:
    @patch("gaol.networking.executor.subprocess.run")
    def test_from_addr_rule(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        route = PolicyRoute(table_id=200, priority=100, from_addr="10.200.1.2")
        result = add_policy_route(route)
        assert result.status == StepStatus.OK
        args = mock_run.call_args[0][0]
        assert "ip" in args
        assert "rule" in args
        assert "from" in args
        assert "10.200.1.2" in args

    @patch("gaol.networking.executor.subprocess.run")
    def test_iif_rule(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        route = PolicyRoute(table_id=200, priority=100, iif="ve-research")
        result = add_policy_route(route)
        assert result.status == StepStatus.OK
        args = mock_run.call_args[0][0]
        assert "iif" in args
        assert "ve-research" in args

    @patch("gaol.networking.executor.subprocess.run")
    def test_already_exists_is_ok(self, mock_run):
        m = MagicMock()
        m.returncode = 2
        m.stdout = ""
        m.stderr = "RTNETLINK answers: File exists"
        mock_run.return_value = m
        route = PolicyRoute(table_id=200, priority=100, from_addr="10.200.1.2")
        result = add_policy_route(route)
        assert result.status == StepStatus.OK

    def test_no_match_skipped(self):
        route = PolicyRoute(table_id=200, priority=100)
        result = add_policy_route(route)
        assert result.status == StepStatus.SKIPPED


class TestRemovePolicyRoute:
    @patch("gaol.networking.executor.subprocess.run")
    def test_remove_from_addr(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        route = PolicyRoute(table_id=200, priority=100, from_addr="10.200.1.2")
        result = remove_policy_route(route)
        assert result.status == StepStatus.OK
        args = mock_run.call_args[0][0]
        assert "del" in args

    @patch("gaol.networking.executor.subprocess.run")
    def test_remove_nonexistent_is_skipped(self, mock_run):
        m = MagicMock()
        m.returncode = 2
        m.stdout = ""
        m.stderr = "No such file or directory"
        mock_run.return_value = m
        route = PolicyRoute(table_id=200, priority=100, from_addr="10.200.1.2")
        result = remove_policy_route(route)
        assert result.status == StepStatus.SKIPPED


class TestSetRouteGateway:
    @patch("gaol.networking.executor.subprocess.run")
    def test_sets_gateway(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        route = PolicyRoute(table_id=200, priority=100, gateway="10.200.2.1")
        result = set_route_gateway(route)
        assert result.status == StepStatus.OK
        args = mock_run.call_args[0][0]
        assert "route" in args
        assert "replace" in args
        assert "10.200.2.1" in args

    def test_no_gateway_skipped(self):
        route = PolicyRoute(table_id=200, priority=100)
        result = set_route_gateway(route)
        assert result.status == StepStatus.SKIPPED


class TestEnableIpForward:
    @patch("gaol.networking.executor.subprocess.run")
    def test_enables_forwarding(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        result = enable_ip_forward()
        assert result.status == StepStatus.OK
        args = mock_run.call_args[0][0]
        assert "sysctl" in args
        assert "net.ipv4.ip_forward=1" in args


class TestRemoveSystemdNatMasquerade:
    @patch("gaol.networking.executor.subprocess.run")
    def test_removes_table(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        result = remove_systemd_nat_masquerade()
        assert result.status == StepStatus.OK

    @patch("gaol.networking.executor.subprocess.run")
    def test_table_not_found_is_skipped(self, mock_run):
        m = MagicMock()
        m.returncode = 1
        m.stdout = ""
        m.stderr = "Error: No such file or directory"
        mock_run.return_value = m
        result = remove_systemd_nat_masquerade()
        assert result.status == StepStatus.SKIPPED


class TestApplyPlan:
    @patch("gaol.networking.executor.subprocess.run")
    def test_host_archetype_noop(self, mock_run):
        plan = resolve_host()
        result = apply_plan(plan)
        assert result.ok is True
        assert len(result.steps) == 1
        mock_run.assert_not_called()

    @patch("gaol.networking.executor.subprocess.run")
    def test_shared_archetype_noop(self, mock_run):
        plan = resolve_shared()
        result = apply_plan(plan)
        assert result.ok is True
        mock_run.assert_not_called()

    @patch("gaol.networking.executor.subprocess.run")
    def test_bridged_writes_no_networkd(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        plan = resolve_bridged()
        result = apply_plan(plan)
        assert result.ok is True
        # Bridged has no host networkd configs or nftables
        assert len(result.steps) == 0 or all(s.ok for s in result.steps)

    @patch("gaol.networking.executor.subprocess.run")
    def test_vpn_exit_full_setup(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        veth = VethConfig(
            host_iface="ve-research",
            container_ip="10.200.1.2",
            host_ip="10.200.1.1",
            subnet="10.200.1.0/30",
        )
        plan = resolve_vpn_exit(
            container_name="research",
            veth=veth,
            table_id=200,
            priority=100,
            vpn_container_name="res-vpn",
            vpn_config_file="/etc/wireguard/mullvad.conf",
        )
        result = apply_plan(plan)
        assert result.ok is True

        step_names = [s.name for s in result.steps]
        # Should have: ip_forward, networkd write, networkd reload,
        # systemd-nat removal, 2x nftables, 2x ip-rule
        assert "sysctl:ip_forward" in step_names
        assert any("networkd:" in n for n in step_names)
        assert any("nftables:" in n for n in step_names)
        assert any("ip-rule:" in n for n in step_names)

    @patch("gaol.networking.executor.subprocess.run")
    def test_vpn_exit_partial_failure(self, mock_run):
        """If one step fails, execution continues but result reports failure."""
        call_count = [0]

        def side_effect(*args, **kwargs):
            call_count[0] += 1
            m = MagicMock()
            # Fail on the 3rd call (networkd reload)
            if call_count[0] == 3:
                m.returncode = 1
                m.stdout = ""
                m.stderr = "reload failed"
            else:
                m.returncode = 0
                m.stdout = ""
                m.stderr = ""
            return m

        mock_run.side_effect = side_effect

        veth = VethConfig(
            host_iface="ve-test",
            container_ip="10.200.1.2",
            host_ip="10.200.1.1",
            subnet="10.200.1.0/30",
        )
        plan = resolve_vpn_exit(
            container_name="test",
            veth=veth,
            table_id=200,
            priority=100,
            vpn_container_name="test-vpn",
            vpn_config_file="/etc/wireguard/test.conf",
        )
        result = apply_plan(plan)
        assert result.ok is False
        assert len(result.failed_steps) >= 1


class TestTeardownPlan:
    @patch("gaol.networking.executor.subprocess.run")
    def test_host_archetype_noop(self, mock_run):
        plan = resolve_host()
        result = teardown_plan(plan)
        assert result.ok is True
        mock_run.assert_not_called()

    @patch("gaol.networking.executor.subprocess.run")
    def test_vpn_exit_teardown(self, mock_run):
        mock_run.return_value = _mock_run_ok()
        veth = VethConfig(
            host_iface="ve-research",
            container_ip="10.200.1.2",
            host_ip="10.200.1.1",
            subnet="10.200.1.0/30",
        )
        plan = resolve_vpn_exit(
            container_name="research",
            veth=veth,
            table_id=200,
            priority=100,
            vpn_container_name="res-vpn",
            vpn_config_file="/etc/wireguard/mullvad.conf",
        )
        result = teardown_plan(plan)
        assert result.ok is True

        step_names = [s.name for s in result.steps]
        assert any("flush" in n for n in step_names)
        assert any("ip-rule:rm" in n for n in step_names)
        assert any("nftables:rm" in n for n in step_names)
        assert any("networkd:rm" in n for n in step_names)
