"""Tests for NX-OS scrapli SSH transport.

Validates the NxosScrapliTransport class and its integration with
NxosDriver, including session reuse, failover from NX-API to SSH,
authentication failure handling, and graceful degradation when scrapli
is not installed.
"""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from network_mcp.drivers.cisco_nxos import HAS_SCRAPLI, NxosDriver, NxosScrapliTransport

# --- NxosScrapliTransport unit tests ---


class TestNxosScrapliTransportInit:
    """Test NxosScrapliTransport initialization."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_init_creates_driver(self):
        """Constructor creates a ScrapliNXOSDriver with correct params."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            transport = NxosScrapliTransport(
                host="10.0.0.1",
                username="admin",
                password="cisco123",
                port=22,
            )
            mock_cls.assert_called_once()
            call_kwargs = mock_cls.call_args[1]
            assert call_kwargs["host"] == "10.0.0.1"
            assert call_kwargs["auth_username"] == "admin"
            assert call_kwargs["auth_password"] == "cisco123"
            assert call_kwargs["port"] == 22
            assert call_kwargs["auth_strict_key"] is True
            assert call_kwargs["transport"] == "system"
            assert call_kwargs["timeout_socket"] == 10
            assert call_kwargs["timeout_ops"] == 30
            assert transport.is_alive is False

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_init_with_private_key(self):
        """Private key is passed to driver when provided."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            NxosScrapliTransport(
                host="10.0.0.1",
                username="admin",
                password="",
                auth_private_key="/home/user/.ssh/id_rsa",
            )
            call_kwargs = mock_cls.call_args[1]
            assert call_kwargs["auth_private_key"] == "/home/user/.ssh/id_rsa"

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_init_strict_key_disabled(self):
        """Host key verification can be disabled."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            NxosScrapliTransport(
                host="10.0.0.1",
                username="admin",
                password="cisco123",
                auth_strict_key=False,
            )
            call_kwargs = mock_cls.call_args[1]
            assert call_kwargs["auth_strict_key"] is False

    def test_init_without_scrapli_raises(self):
        """ImportError raised when scrapli is not installed."""
        with patch("network_mcp.drivers.cisco_nxos.HAS_SCRAPLI", False):
            with pytest.raises(ImportError, match="scrapli not installed"):
                NxosScrapliTransport(
                    host="10.0.0.1",
                    username="admin",
                    password="cisco123",
                )


class TestNxosScrapliTransportSendCommand:
    """Test NxosScrapliTransport.send_command()."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_send_command_returns_output(self):
        """send_command() returns the result string from scrapli."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            mock_response = MagicMock()
            mock_response.failed = False
            mock_response.result = "Cisco Nexus Operating System (NX-OS) Software"
            mock_driver.send_command.return_value = mock_response

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            # Mark as connected to skip auto-open
            transport._connected = True

            output = transport.send_command("show version")
            assert output == "Cisco Nexus Operating System (NX-OS) Software"
            mock_driver.send_command.assert_called_once_with("show version")

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_send_command_auto_opens(self):
        """send_command() calls open() if not connected."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            mock_response = MagicMock()
            mock_response.failed = False
            mock_response.result = "output"
            mock_driver.send_command.return_value = mock_response

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            assert transport.is_alive is False

            transport.send_command("show version")
            mock_driver.open.assert_called_once()
            assert transport.is_alive is True

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_send_command_failed_raises(self):
        """send_command() raises RuntimeError when scrapli reports failure."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            mock_response = MagicMock()
            mock_response.failed = True
            mock_response.channel_input = "show bad-command"
            mock_driver.send_command.return_value = mock_response

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            transport._connected = True

            with pytest.raises(RuntimeError, match="Command failed"):
                transport.send_command("show bad-command")


class TestNxosScrapliTransportSendConfig:
    """Test NxosScrapliTransport.send_config()."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_send_config_returns_output(self):
        """send_config() sends configs via scrapli and returns result."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            mock_response = MagicMock()
            mock_response.failed = False
            mock_response.result = ""
            mock_driver.send_configs.return_value = mock_response

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            transport._connected = True

            output = transport.send_config(["vlan 100", "name SERVERS"])
            assert output == ""
            mock_driver.send_configs.assert_called_once_with(["vlan 100", "name SERVERS"])

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_send_config_failed_raises(self):
        """send_config() raises RuntimeError on failure."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            mock_response = MagicMock()
            mock_response.failed = True
            mock_response.result = "Invalid input"
            mock_driver.send_configs.return_value = mock_response

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            transport._connected = True

            with pytest.raises(RuntimeError, match="Config commands failed"):
                transport.send_config(["invalid-command"])


class TestNxosScrapliTransportLifecycle:
    """Test open/close lifecycle."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_open_sets_connected(self):
        """open() marks transport as connected."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            assert transport.is_alive is False

            transport.open()
            mock_driver.open.assert_called_once()
            assert transport.is_alive is True

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_close_disconnects(self):
        """close() closes the driver and marks as disconnected."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            transport._connected = True

            transport.close()
            mock_driver.close.assert_called_once()
            assert transport.is_alive is False

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_close_when_not_connected_is_noop(self):
        """close() does nothing when not connected."""
        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_cls:
            mock_driver = MagicMock()
            mock_cls.return_value = mock_driver

            transport = NxosScrapliTransport(host="10.0.0.1", username="admin", password="cisco123")
            transport.close()
            mock_driver.close.assert_not_called()


# --- NxosDriver SSH integration tests ---


class TestNxosDriverScrapliIntegration:
    """Test NxosDriver SSH fallback using scrapli transport."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_nxapi_to_ssh_failover(self):
        """When NX-API fails, driver creates scrapli SSH transport."""
        driver = NxosDriver()

        with (
            patch("network_mcp.drivers.cisco_nxos.httpx") as mock_httpx,
            patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls,
        ):
            mock_client = MagicMock()
            mock_client.post.side_effect = Exception("Connection refused")
            mock_httpx.Client.return_value = mock_client

            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver

            driver.connect(host="10.0.0.1", username="admin", password="cisco123")

        assert driver.is_connected()
        assert driver._transport == "ssh"
        assert driver._ssh_transport is not None

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_ssh_direct_creates_scrapli_transport(self):
        """Direct SSH transport creates scrapli transport."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="cisco123")

        assert driver.is_connected()
        assert driver._transport == "ssh"
        assert driver._ssh_transport is not None

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_ssh_session_reuse(self):
        """Second command reuses the same SSH session (no new open)."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver

            mock_response = MagicMock()
            mock_response.failed = False
            mock_response.result = "output1"
            mock_scrapli_driver.send_command.return_value = mock_response

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="cisco123")
            # Simulate the transport being connected already
            driver._ssh_transport._connected = True

            # First command
            result1 = driver._ssh_exec("show version")
            # Second command
            mock_response.result = "output2"
            result2 = driver._ssh_exec("show interfaces")

            assert result1 == "output1"
            assert result2 == "output2"
            # ScrapliNXOSDriver was only instantiated once (session reuse)
            assert mock_scrapli_cls.call_count == 1

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_ssh_auth_failure(self):
        """Authentication failure raises RuntimeError."""
        from scrapli.exceptions import ScrapliAuthenticationFailed as AuthFailed

        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver
            mock_scrapli_driver.send_command.side_effect = AuthFailed("Auth failed")

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="wrong")
            driver._ssh_transport._connected = True

            with pytest.raises(RuntimeError, match="SSH command failed"):
                driver._ssh_exec("show version")

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_ssh_timeout_raises_timeout_error(self):
        """Scrapli timeout is converted to TimeoutError."""
        from scrapli.exceptions import ScrapliTimeout as Timeout

        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver
            mock_scrapli_driver.send_command.side_effect = Timeout("Timed out")

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="cisco123")
            driver._ssh_transport._connected = True

            with pytest.raises(TimeoutError, match="timed out"):
                driver._ssh_exec("show version")

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_close_cleans_up_scrapli(self):
        """close() cleans up both NX-API and scrapli transports."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="cisco123")
            assert driver._ssh_transport is not None
            driver._ssh_transport._connected = True

            driver.close()
            mock_scrapli_driver.close.assert_called_once()
            assert driver._ssh_transport is None
            assert not driver.is_connected()

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_ssh_config_via_scrapli(self):
        """Config commands are sent via scrapli send_configs."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_driver = MagicMock()
            mock_scrapli_cls.return_value = mock_scrapli_driver

            mock_response = MagicMock()
            mock_response.failed = False
            mock_response.result = ""
            mock_scrapli_driver.send_configs.return_value = mock_response

            driver.connect(host="10.0.0.1", transport="ssh", username="admin", password="cisco123")
            driver._ssh_transport._connected = True

            result = driver._ssh_config(["vlan 100", "name SERVERS"])
            assert result == [""]
            mock_scrapli_driver.send_configs.assert_called_once_with(["vlan 100", "name SERVERS"])


class TestNxosDriverNoScrapli:
    """Test graceful degradation when scrapli is not installed."""

    def test_ssh_fallback_without_scrapli_logs_warning(self):
        """When scrapli is not installed, SSH fallback logs a warning."""
        driver = NxosDriver()

        with (
            patch("network_mcp.drivers.cisco_nxos.HAS_SCRAPLI", False),
            patch("network_mcp.drivers.cisco_nxos.HAS_HTTPX", False),
            patch("network_mcp.drivers.cisco_nxos.logger") as mock_logger,
        ):
            driver.connect(host="10.0.0.1", username="admin", password="cisco123")

        assert driver.is_connected()
        assert driver._transport == "ssh"
        assert driver._ssh_transport is None
        mock_logger.warning.assert_any_call(
            "scrapli not installed — SSH fallback unavailable for %s. Install with: pip install latticio[ssh]",
            "10.0.0.1",
        )

    def test_ssh_exec_without_transport_raises(self):
        """_ssh_exec raises ConnectionError when scrapli transport is None."""
        driver = NxosDriver()
        driver._connected = True
        driver._ssh_host = "10.0.0.1"
        driver._ssh_username = "admin"
        driver._ssh_transport = None

        with pytest.raises(ConnectionError, match="SSH transport not available"):
            driver._ssh_exec("show version")

    def test_ssh_config_without_transport_raises(self):
        """_ssh_config raises ConnectionError when scrapli transport is None."""
        driver = NxosDriver()
        driver._connected = True
        driver._ssh_transport = None

        with pytest.raises(ConnectionError, match="SSH transport not available"):
            driver._ssh_config(["vlan 100"])

    def test_ssh_show_without_transport_returns_error(self):
        """_ssh_show returns error dict when SSH transport unavailable."""
        driver = NxosDriver()
        driver._connected = True
        driver._ssh_host = "10.0.0.1"
        driver._ssh_username = "admin"
        driver._ssh_transport = None

        results = driver._ssh_show(["show version"])
        assert len(results) == 1
        assert "error" in results[0]
        assert "SSH transport not available" in results[0]["error"]


class TestNxosDriverSshKwargsPassthrough:
    """Test that SSH-specific kwargs are passed through to scrapli."""

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_private_key_passthrough(self):
        """auth_private_key kwarg is stored and passed to scrapli."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_cls.return_value = MagicMock()

            driver.connect(
                host="10.0.0.1",
                transport="ssh",
                username="admin",
                password="",
                auth_private_key="/home/user/.ssh/id_rsa",
            )

            call_kwargs = mock_scrapli_cls.call_args[1]
            assert call_kwargs["auth_private_key"] == "/home/user/.ssh/id_rsa"

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_strict_key_disabled(self):
        """ssh_strict_key=False disables host key verification."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_cls.return_value = MagicMock()

            driver.connect(
                host="10.0.0.1",
                transport="ssh",
                username="admin",
                password="cisco123",
                ssh_strict_key=False,
            )

            call_kwargs = mock_scrapli_cls.call_args[1]
            assert call_kwargs["auth_strict_key"] is False

    @pytest.mark.skipif(not HAS_SCRAPLI, reason="scrapli not installed")
    def test_custom_known_hosts_file(self):
        """ssh_known_hosts_file kwarg is passed to scrapli."""
        driver = NxosDriver()

        with patch("network_mcp.drivers.cisco_nxos.ScrapliNXOSDriver") as mock_scrapli_cls:
            mock_scrapli_cls.return_value = MagicMock()

            driver.connect(
                host="10.0.0.1",
                transport="ssh",
                username="admin",
                password="cisco123",
                ssh_known_hosts_file="/custom/known_hosts",
            )

            call_kwargs = mock_scrapli_cls.call_args[1]
            assert call_kwargs["ssh_known_hosts_file"] == "/custom/known_hosts"
