"""Tests for main.py - CLI command integration."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from cli.main import app
from cli.config import LocalBeacon, LocalState
from cli.exceptions import BeaconAPIError, BeaconNotFoundError

from .conftest import SAMPLE_BEACON, SAMPLE_BEACON_LIST

runner = CliRunner()


# ---------------------------------------------------------------------------
# version
# ---------------------------------------------------------------------------


class TestVersion:
    def test_prints_version(self):
        result = runner.invoke(app, ["version"])
        assert result.exit_code == 0
        from cli import __version__
        assert __version__ in result.output


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------


class TestList:
    def test_shows_beacon_table(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.list.return_value = SAMPLE_BEACON_LIST

        with patch("cli.main._api", return_value=mock_api):
            result = runner.invoke(app, ["list"])

        assert result.exit_code == 0
        # Rich tables may render differently in test runners; verify via mock calls
        mock_api.list.assert_called_once()
        # The table border is a reliable marker that print_beacon_table ran
        assert "━" in result.output or "─" in result.output or "Name" in result.output

    def test_shows_empty_message_when_no_beacons(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.list.return_value = {"beacons": []}

        with patch("cli.main._api", return_value=mock_api):
            result = runner.invoke(app, ["list"])

        assert result.exit_code == 0
        assert "No beacons found" in result.output

    def test_api_error_exits_with_error(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.list.side_effect = BeaconAPIError("Server error", 500)

        with patch("cli.main._api", return_value=mock_api):
            result = runner.invoke(app, ["list"])

        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# status
# ---------------------------------------------------------------------------


class TestStatus:
    def test_shows_beacon_detail(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.get.return_value = SAMPLE_BEACON

        with patch("cli.main._api", return_value=mock_api), \
             patch("cli.wireguard.is_up", return_value=False), \
             patch("cli.rathole.is_running", return_value=False):
            result = runner.invoke(app, ["status", "beacon-id-001"])

        assert result.exit_code == 0
        assert "test-beacon" in result.output

    def test_shows_local_service_status(self, state_dir, populated_state):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.get.return_value = SAMPLE_BEACON

        with patch("cli.main._api", return_value=mock_api), \
             patch("cli.wireguard.is_up", return_value=True), \
             patch("cli.rathole.is_running", return_value=True):
            result = runner.invoke(app, ["status", "beacon-id-001"])

        assert result.exit_code == 0
        # Both services shown as up
        assert "up" in result.output
        assert "running" in result.output

    def test_not_found_exits_with_error(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.get.side_effect = BeaconNotFoundError("Not found", 404)

        with patch("cli.main._api", return_value=mock_api):
            result = runner.invoke(app, ["status", "ghost-id"])

        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# stop
# ---------------------------------------------------------------------------


class TestStop:
    def test_stops_running_beacon(self, state_dir, populated_state):
        with patch("cli.rathole.stop", return_value=True) as mock_rat, \
             patch("cli.wireguard.down") as mock_wg:
            result = runner.invoke(app, ["stop", "beacon-id-001"])

        assert result.exit_code == 0
        mock_rat.assert_called_once_with("beacon-id-001")
        mock_wg.assert_called_once_with("wg0")

    def test_stop_unknown_beacon_exits(self, state_dir):
        result = runner.invoke(app, ["stop", "ghost-beacon"])
        assert result.exit_code == 1
        assert "No local state" in result.output


# ---------------------------------------------------------------------------
# delete
# ---------------------------------------------------------------------------


class TestDelete:
    def test_delete_with_yes_flag(self, state_dir, populated_state):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.delete.return_value = None

        with patch("cli.main._api", return_value=mock_api), \
             patch("cli.rathole.is_running", return_value=False), \
             patch("cli.wireguard.down"), \
             patch("cli.wireguard.remove_config"):
            result = runner.invoke(app, ["delete", "beacon-id-001", "--yes"])

        assert result.exit_code == 0
        mock_api.delete.assert_called_once_with("beacon-id-001")

    def test_delete_aborts_without_yes(self, state_dir):
        result = runner.invoke(app, ["delete", "beacon-id-001"], input="n\n")
        assert result.exit_code == 0  # user cancelled

    def test_delete_api_error_exits(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.delete.side_effect = BeaconNotFoundError("Not found", 404)

        with patch("cli.main._api", return_value=mock_api), \
             patch("cli.rathole.is_running", return_value=False):
            result = runner.invoke(app, ["delete", "ghost-id", "--yes"])

        assert result.exit_code == 1


# ---------------------------------------------------------------------------
# update
# ---------------------------------------------------------------------------


class TestUpdate:
    def test_update_invalid_name_aborts(self, state_dir):
        result = runner.invoke(app, ["update", "beacon-id-001", "--name", "bad name!"])
        assert result.exit_code == 1
        assert "letters" in result.output.lower() or "only contain" in result.output.lower()

    def test_update_name(self, state_dir):
        mock_api = MagicMock()
        mock_api.__enter__ = lambda s: mock_api
        mock_api.__exit__ = MagicMock(return_value=False)
        mock_api.update.return_value = {**SAMPLE_BEACON, "name": "new-name"}

        with patch("cli.main._api", return_value=mock_api):
            result = runner.invoke(app, ["update", "beacon-id-001", "--name", "new-name"])

        assert result.exit_code == 0
        mock_api.update.assert_called_once_with("beacon-id-001", name="new-name")

    def test_update_nothing_warns(self, state_dir):
        result = runner.invoke(app, ["update", "beacon-id-001"])
        assert result.exit_code == 0
        assert "Nothing to update" in result.output


# ---------------------------------------------------------------------------
# test-connection
# ---------------------------------------------------------------------------


# TODO @0xacb deactivated for now (test command disabled)
# class TestTestConnection:
#     def test_shows_reachable(self, state_dir):
#         mock_api = MagicMock()
#         mock_api.__enter__ = lambda s: mock_api
#         mock_api.__exit__ = MagicMock(return_value=False)
#         mock_api.test_connection.return_value = {"reachable": True, "latency_ms": 5}
#
#         with patch("cli.main._api", return_value=mock_api):
#             result = runner.invoke(app, ["test", "beacon-id-001", "192.168.1.1"])
#
#         assert result.exit_code == 0
#         assert "reachable" in result.output.lower()
#
#     def test_shows_not_reachable(self, state_dir):
#         mock_api = MagicMock()
#         mock_api.__enter__ = lambda s: mock_api
#         mock_api.__exit__ = MagicMock(return_value=False)
#         mock_api.test_connection.return_value = {"reachable": False, "detail": "timeout"}
#
#         with patch("cli.main._api", return_value=mock_api):
#             result = runner.invoke(app, ["test", "beacon-id-001", "10.0.0.99"])
#
#         assert result.exit_code == 0
#         assert "NOT reachable" in result.output


# ---------------------------------------------------------------------------
# Missing credentials
# ---------------------------------------------------------------------------


class TestMissingCredentials:
    def test_list_exits_when_no_key(self, monkeypatch):
        monkeypatch.delenv("ETHIACK_API_KEY", raising=False)
        monkeypatch.delenv("ETHIACK_API_SECRET", raising=False)
        result = runner.invoke(app, ["list"])
        assert result.exit_code == 1
        assert "ETHIACK_API_KEY" in result.output or "ETHIACK_API_KEY" in result.stderr
