"""Tests for ``ilum access`` sub-commands."""

from __future__ import annotations

import socket
from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from ilum.cli.main import app
from ilum.core.helm import HelmResult
from ilum.core.release import ReleaseManager, ReleasePlan
from ilum.core.safety import DriftResult, ValuesDiff
from ilum.errors import ClusterConnectionError


@pytest.fixture()
def runner() -> CliRunner:
    return CliRunner()


@pytest.fixture()
def mock_mgr() -> MagicMock:
    mock = MagicMock(spec=ReleaseManager)
    mock.helm = MagicMock()
    mock.helm.namespace = "default"
    mock.release_exists.return_value = True
    mock.preview_command.return_value = ["helm", "upgrade", "ilum", "ilum/ilum"]
    return mock


def _make_plan(
    action: str = "upgrade",
    set_flags: list[str] | None = None,
    has_diff: bool = True,
) -> ReleasePlan:
    diff = ValuesDiff(
        added={"ilum-ui.ingress.enabled": True} if has_diff else {},
        changed={},
        removed={},
    )
    return ReleasePlan(
        action=action,
        release="ilum",
        namespace="default",
        chart="ilum/ilum",
        set_flags=set_flags or [],
        effective_diff=diff,
        drift=DriftResult(has_drift=False, snapshot_exists=False, diff=None, live_values={}),
    )


# ---------------------------------------------------------------------------
# ilum access --help
# ---------------------------------------------------------------------------


class TestAccessHelp:
    def test_access_help(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["access", "--help"])
        assert result.exit_code == 0
        assert "ingress" in result.output
        assert "open" in result.output

    def test_ingress_help(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["access", "ingress", "--help"])
        assert result.exit_code == 0
        assert "enable" in result.output
        assert "disable" in result.output
        assert "show" in result.output


# ---------------------------------------------------------------------------
# ilum access open
# ---------------------------------------------------------------------------


class TestAccessOpen:
    def test_open_help(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["access", "open", "--help"])
        assert result.exit_code == 0
        assert "--port" in result.output
        assert "--no-browser" in result.output
        assert "--address" in result.output
        assert "--access" in result.output

    # -- port-forward tests (pass --access port-forward) --------------------

    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=True)
    def test_port_in_use_error(self, mock_port: MagicMock, runner: CliRunner) -> None:
        result = runner.invoke(app, ["access", "open", "--access", "port-forward"])
        assert result.exit_code == 1
        assert "already in use" in result.output

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_starts_subprocess(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 0
        proc.returncode = 0
        mock_popen.return_value = proc

        result = runner.invoke(app, ["access", "open", "--access", "port-forward", "--no-browser"])
        assert result.exit_code == 0
        mock_popen.assert_called_once()
        cmd = mock_popen.call_args[0][0]
        assert "kubectl" in cmd
        assert "port-forward" in cmd
        assert "svc/ilum-ui" in cmd

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_custom_port_and_address(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 0
        proc.returncode = 0
        mock_popen.return_value = proc

        result = runner.invoke(
            app,
            [
                "access",
                "open",
                "--access",
                "port-forward",
                "--port",
                "8080",
                "--address",
                "0.0.0.0",
                "--no-browser",
            ],
        )
        assert result.exit_code == 0
        cmd = mock_popen.call_args[0][0]
        assert "--address=0.0.0.0" in cmd
        assert "8080:9777" in cmd

    def test_port_check_sets_so_reuseaddr(self) -> None:
        """SO_REUSEADDR avoids false positives from TIME_WAIT sockets."""
        with patch("ilum.cli.access_cmd.socket.socket") as mock_socket:
            sock = MagicMock()
            mock_socket.return_value.__enter__.return_value = sock
            sock.bind.return_value = None

            from ilum.cli.access_cmd import _is_port_in_use

            _is_port_in_use(9777)

            sock.setsockopt.assert_called_once_with(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_nonzero_exit_code_on_port_forward_failure(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 1
        proc.returncode = 1
        proc.stderr.read.return_value = b'error: services "not-existing-ui" not found'
        mock_popen.return_value = proc

        result = runner.invoke(
            app,
            [
                "access",
                "open",
                "--access",
                "port-forward",
                "--release",
                "not-existing",
                "--no-browser",
            ],
        )
        assert result.exit_code == 1
        assert "not found" in result.output or "failed" in result.output

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_nonzero_exit_code_no_stderr(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 1
        proc.returncode = 1
        proc.stderr = None
        mock_popen.return_value = proc

        result = runner.invoke(app, ["access", "open", "--access", "port-forward", "--no-browser"])
        assert result.exit_code == 1
        assert "failed" in result.output

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_zero_exit_code_on_success(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 0
        proc.returncode = 0
        mock_popen.return_value = proc

        result = runner.invoke(app, ["access", "open", "--access", "port-forward", "--no-browser"])
        assert result.exit_code == 0

    @patch("ilum.cli.access_cmd.webbrowser.open")
    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_open_browser_no_stderr_leak(
        self,
        mock_port: MagicMock,
        mock_popen: MagicMock,
        mock_browser: MagicMock,
        runner: CliRunner,
    ) -> None:
        proc = MagicMock()
        proc.wait.return_value = 0
        proc.returncode = 0
        mock_popen.return_value = proc

        result = runner.invoke(app, ["access", "open", "--access", "port-forward"])
        assert result.exit_code == 0
        assert "symbol lookup error" not in result.output

    # -- access port-forward flag -------------------------------------------

    @patch("ilum.cli.access_cmd.subprocess.Popen")
    @patch("ilum.cli.access_cmd._is_port_in_use", return_value=False)
    def test_access_port_forward_flag(
        self, mock_port: MagicMock, mock_popen: MagicMock, runner: CliRunner
    ) -> None:
        """--access port-forward uses the existing port-forward path."""
        proc = MagicMock()
        proc.wait.return_value = 0
        proc.returncode = 0
        mock_popen.return_value = proc

        result = runner.invoke(app, ["access", "open", "--access", "port-forward", "--no-browser"])
        assert result.exit_code == 0
        mock_popen.assert_called_once()
        cmd = mock_popen.call_args[0][0]
        assert "port-forward" in cmd

    # -- nodeport tests -----------------------------------------------------

    @patch("ilum.cli.access_cmd.KubeClient")
    def test_default_access_is_nodeport(self, mock_kube_cls: MagicMock, runner: CliRunner) -> None:
        """With no --access flag, nodeport path is taken."""
        mock_k8s = MagicMock()
        mock_k8s.get_service_node_port.return_value = 31777
        mock_k8s.get_node_address.return_value = "192.168.1.10"
        mock_kube_cls.return_value = mock_k8s

        result = runner.invoke(app, ["access", "open", "--no-browser"])
        assert result.exit_code == 0
        assert "192.168.1.10:31777" in result.output
        mock_k8s.get_service_node_port.assert_called_once()
        mock_k8s.get_node_address.assert_called_once()

    @patch("ilum.cli.access_cmd.KubeClient")
    def test_nodeport_opens_browser_with_correct_url(
        self, mock_kube_cls: MagicMock, runner: CliRunner
    ) -> None:
        """Nodeport mode constructs the correct URL from node IP and nodePort."""
        mock_k8s = MagicMock()
        mock_k8s.get_service_node_port.return_value = 31777
        mock_k8s.get_node_address.return_value = "10.0.0.5"
        mock_kube_cls.return_value = mock_k8s

        result = runner.invoke(app, ["access", "open", "--access", "nodeport", "--no-browser"])
        assert result.exit_code == 0
        assert "http://10.0.0.5:31777" in result.output

    @patch("ilum.cli.access_cmd.KubeClient")
    def test_nodeport_service_not_found(self, mock_kube_cls: MagicMock, runner: CliRunner) -> None:
        """Error when service has no nodePort."""
        mock_k8s = MagicMock()
        mock_k8s.get_service_node_port.side_effect = ClusterConnectionError(
            "Service 'ilum-ui' has no NodePort allocated."
        )
        mock_kube_cls.return_value = mock_k8s

        result = runner.invoke(app, ["access", "open", "--access", "nodeport", "--no-browser"])
        assert result.exit_code == 1
        assert "NodePort" in result.output

    @patch("ilum.cli.access_cmd.KubeClient")
    def test_nodeport_no_nodes(self, mock_kube_cls: MagicMock, runner: CliRunner) -> None:
        """Error when no nodes available."""
        mock_k8s = MagicMock()
        mock_k8s.get_service_node_port.return_value = 31777
        mock_k8s.get_node_address.side_effect = ClusterConnectionError(
            "No nodes found in the cluster."
        )
        mock_kube_cls.return_value = mock_k8s

        result = runner.invoke(app, ["access", "open", "--access", "nodeport", "--no-browser"])
        assert result.exit_code == 1
        assert "No nodes" in result.output

    # -- ingress tests ------------------------------------------------------

    @patch("ilum.cli.access_cmd.KubeClient")
    def test_access_ingress_flag(self, mock_kube_cls: MagicMock, runner: CliRunner) -> None:
        """--access ingress reads ingress host and opens browser."""
        mock_k8s = MagicMock()
        mock_kube_cls.return_value = mock_k8s

        # Build a mock ingress object
        mock_rule = MagicMock()
        mock_rule.host = "ilum.example.com"
        mock_ingress = MagicMock()
        mock_ingress.spec.rules = [mock_rule]
        mock_ingress.spec.tls = None

        mock_net_api = MagicMock()
        mock_net_api.read_namespaced_ingress.return_value = mock_ingress

        with patch("ilum.cli.access_cmd.NetworkingV1Api", return_value=mock_net_api):
            result = runner.invoke(app, ["access", "open", "--access", "ingress", "--no-browser"])

        assert result.exit_code == 0
        assert "http://ilum.example.com" in result.output

    # -- invalid access type ------------------------------------------------

    def test_invalid_access_type(self, runner: CliRunner) -> None:
        """Unknown access type produces an error."""
        result = runner.invoke(app, ["access", "open", "--access", "bogus", "--no-browser"])
        assert result.exit_code == 1
        assert "Invalid access type" in result.output


# ---------------------------------------------------------------------------
# ilum access ingress enable
# ---------------------------------------------------------------------------


class TestAccessIngressEnable:
    def test_requires_host(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["access", "ingress", "enable"])
        assert result.exit_code != 0

    @patch("ilum.cli.access_cmd._build_manager")
    def test_sets_correct_flags(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        plan = _make_plan(set_flags=["ilum-ui.ingress.enabled=true"])
        mock_mgr.plan_upgrade.return_value = plan
        mock_mgr.execute.return_value = HelmResult(returncode=0, stdout="", stderr="", command=[])
        mock_build.return_value = mock_mgr

        result = runner.invoke(
            app, ["access", "ingress", "enable", "--host", "ilum.example.com", "--yes"]
        )
        assert result.exit_code == 0
        call_kwargs = mock_mgr.plan_upgrade.call_args
        flags = call_kwargs.kwargs.get("set_flags") or call_kwargs[1].get("set_flags", [])
        assert "ilum-ui.ingress.enabled=true" in flags
        assert "ilum-ui.ingress.host=ilum.example.com" in flags

    @patch("ilum.cli.access_cmd._build_manager")
    def test_tls_flags(self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner) -> None:
        plan = _make_plan()
        mock_mgr.plan_upgrade.return_value = plan
        mock_mgr.execute.return_value = HelmResult(returncode=0, stdout="", stderr="", command=[])
        mock_build.return_value = mock_mgr

        result = runner.invoke(
            app,
            [
                "access",
                "ingress",
                "enable",
                "--host",
                "ilum.example.com",
                "--tls",
                "--tls-secret",
                "my-tls-secret",
                "--yes",
            ],
        )
        assert result.exit_code == 0
        flags = mock_mgr.plan_upgrade.call_args.kwargs.get(
            "set_flags"
        ) or mock_mgr.plan_upgrade.call_args[1].get("set_flags", [])
        assert "ilum-ui.ingress.tls[0].secretName=my-tls-secret" in flags
        assert "ilum-ui.ingress.tls[0].hosts[0]=ilum.example.com" in flags

    @patch("ilum.cli.access_cmd._build_manager")
    def test_class_flag(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        plan = _make_plan()
        mock_mgr.plan_upgrade.return_value = plan
        mock_mgr.execute.return_value = HelmResult(returncode=0, stdout="", stderr="", command=[])
        mock_build.return_value = mock_mgr

        result = runner.invoke(
            app,
            [
                "access",
                "ingress",
                "enable",
                "--host",
                "ilum.example.com",
                "--class",
                "nginx",
                "--yes",
            ],
        )
        assert result.exit_code == 0
        flags = mock_mgr.plan_upgrade.call_args.kwargs.get(
            "set_flags"
        ) or mock_mgr.plan_upgrade.call_args[1].get("set_flags", [])
        assert "ilum-ui.ingress.className=nginx" in flags

    @patch("ilum.cli.access_cmd._build_manager")
    def test_dry_run(self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner) -> None:
        plan = _make_plan()
        mock_mgr.plan_upgrade.return_value = plan
        mock_build.return_value = mock_mgr

        result = runner.invoke(
            app,
            ["access", "ingress", "enable", "--host", "ilum.example.com", "--dry-run"],
        )
        assert result.exit_code == 0
        assert "Dry-run" in result.output
        mock_mgr.execute.assert_not_called()

    @patch("ilum.cli.access_cmd._build_manager")
    def test_no_changes_needed(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        plan = _make_plan(has_diff=False)
        mock_mgr.plan_upgrade.return_value = plan
        mock_build.return_value = mock_mgr

        result = runner.invoke(
            app, ["access", "ingress", "enable", "--host", "ilum.example.com", "--yes"]
        )
        assert result.exit_code == 0
        assert "No values changes needed" in result.output


# ---------------------------------------------------------------------------
# ilum access ingress disable
# ---------------------------------------------------------------------------


class TestAccessIngressDisable:
    @patch("ilum.cli.access_cmd._build_manager")
    def test_sets_enabled_false(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        plan = _make_plan()
        mock_mgr.plan_upgrade.return_value = plan
        mock_mgr.execute.return_value = HelmResult(returncode=0, stdout="", stderr="", command=[])
        mock_build.return_value = mock_mgr

        result = runner.invoke(app, ["access", "ingress", "disable", "--yes"])
        assert result.exit_code == 0
        flags = mock_mgr.plan_upgrade.call_args.kwargs.get(
            "set_flags"
        ) or mock_mgr.plan_upgrade.call_args[1].get("set_flags", [])
        assert "ilum-ui.ingress.enabled=false" in flags

    @patch("ilum.cli.access_cmd._build_manager")
    def test_dry_run(self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner) -> None:
        plan = _make_plan()
        mock_mgr.plan_upgrade.return_value = plan
        mock_build.return_value = mock_mgr

        result = runner.invoke(app, ["access", "ingress", "disable", "--dry-run"])
        assert result.exit_code == 0
        assert "Dry-run" in result.output


# ---------------------------------------------------------------------------
# ilum access ingress show
# ---------------------------------------------------------------------------


class TestAccessIngressShow:
    @patch("ilum.cli.access_cmd._build_manager")
    def test_shows_enabled_config(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        mock_mgr.fetch_computed_values.return_value = {
            "ilum-ui": {
                "ingress": {
                    "enabled": True,
                    "host": "ilum.example.com",
                    "className": "nginx",
                    "path": "/(.*)",
                    "tls": [
                        {
                            "secretName": "ilum-tls",
                            "hosts": ["ilum.example.com"],
                        }
                    ],
                }
            }
        }
        mock_build.return_value = mock_mgr

        result = runner.invoke(app, ["access", "ingress", "show"])
        assert result.exit_code == 0
        assert "True" in result.output
        assert "ilum.example.com" in result.output
        assert "nginx" in result.output

    @patch("ilum.cli.access_cmd._build_manager")
    def test_shows_disabled_config(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        mock_mgr.fetch_computed_values.return_value = {
            "ilum-ui": {
                "ingress": {
                    "enabled": False,
                    "host": "host",
                    "className": "",
                }
            }
        }
        mock_build.return_value = mock_mgr

        result = runner.invoke(app, ["access", "ingress", "show"])
        assert result.exit_code == 0
        assert "False" in result.output

    @patch("ilum.cli.access_cmd._build_manager")
    def test_json_output(
        self, mock_build: MagicMock, mock_mgr: MagicMock, runner: CliRunner
    ) -> None:
        mock_mgr.fetch_computed_values.return_value = {
            "ilum-ui": {
                "ingress": {
                    "enabled": True,
                    "host": "ilum.example.com",
                }
            }
        }
        mock_build.return_value = mock_mgr

        result = runner.invoke(app, ["--output", "json", "access", "ingress", "show"])
        assert result.exit_code == 0
        assert "ilum.example.com" in result.output
