"""Coverage for main.py: health, restart, list pagination, stop wg error,
delete with local services, update with cidrs/dns, test_connection edge cases."""

from __future__ import annotations

from unittest.mock import MagicMock, call, patch

import pytest
from typer.testing import CliRunner

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

from .conftest import SAMPLE_BEACON

runner = CliRunner()


def _mock_api(return_data=None, side_effect=None):
    m = MagicMock()
    m.__enter__ = lambda s: m
    m.__exit__ = MagicMock(return_value=False)
    if side_effect:
        m.get.side_effect = side_effect
        m.health.side_effect = side_effect
    else:
        m.get.return_value = return_data or SAMPLE_BEACON
        m.health.return_value = None
    return m


# ---------------------------------------------------------------------------
# health --once
# ---------------------------------------------------------------------------


class TestHealthOnce:
    def test_reports_nothing_when_no_beacons(self, state_dir):
        result = runner.invoke(app, ["health", "--once"])
        assert result.exit_code == 0
        assert "No local beacons" in result.output

    def test_reports_active_when_both_services_up(self, state_dir, populated_state):
        with patch("cli.main.report_health") as mock_report, \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.dns_mgr") as mock_dns:
            mock_wg.get_status.return_value = {"active": True, "peers": 1, "latest_handshake": "1 min"}
            mock_rat.is_running.return_value = True
            mock_rat.is_connected.return_value = True
            mock_dns.is_running.return_value = True
            result = runner.invoke(app, ["health", "--once"])
        assert result.exit_code == 0
        mock_report.assert_called_once()
        _, args, kwargs = mock_report.mock_calls[0]
        reported_status = args[2] if len(args) > 2 else kwargs.get("status")
        assert reported_status == "active"

    def test_reports_faulty_when_wireguard_down(self, state_dir, populated_state):
        with patch("cli.main.report_health") as mock_report, \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.dns_mgr") as mock_dns:
            mock_wg.get_status.return_value = {"active": False, "peers": 0, "latest_handshake": None}
            mock_rat.is_running.return_value = True
            mock_rat.is_connected.return_value = False
            mock_dns.is_running.return_value = True
            result = runner.invoke(app, ["health", "--once"])
        assert result.exit_code == 0
        _, args, kwargs = mock_report.mock_calls[0]
        reported_status = args[2] if len(args) > 2 else kwargs.get("status")
        assert reported_status == "faulty"

    def test_reports_faulty_when_rathole_down(self, state_dir, populated_state):
        with patch("cli.main.report_health") as mock_report, \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.dns_mgr") as mock_dns:
            mock_wg.get_status.return_value = {"active": True, "peers": 1, "latest_handshake": None}
            mock_rat.is_running.return_value = False
            mock_rat.is_connected.return_value = False
            mock_dns.is_running.return_value = False
            result = runner.invoke(app, ["health", "--once"])
        assert result.exit_code == 0
        _, args, kwargs = mock_report.mock_calls[0]
        reported_status = args[2] if len(args) > 2 else kwargs.get("status")
        assert reported_status == "faulty"

    def test_api_error_during_health_shows_warning(self, state_dir, populated_state):
        with patch("cli.main.report_health", side_effect=BeaconAPIError("service unavailable", 503)), \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.dns_mgr") as mock_dns:
            mock_wg.get_status.return_value = {"active": True, "peers": 0, "latest_handshake": None}
            mock_rat.is_running.return_value = True
            mock_rat.is_connected.return_value = True
            mock_dns.is_running.return_value = True
            result = runner.invoke(app, ["health", "--once"])
        # Should not crash - errors are caught and shown as warnings
        assert result.exit_code == 0
        assert "failed" in result.output.lower() or "service unavailable" in result.output


# ---------------------------------------------------------------------------
# health continuous (keyboard interrupt)
# ---------------------------------------------------------------------------


class TestHealthContinuous:
    def test_stops_on_keyboard_interrupt(self, state_dir):
        with patch("cli.main.time") as mock_time:
            mock_time.sleep.side_effect = KeyboardInterrupt
            result = runner.invoke(app, ["health", "--interval", "1"])
        assert result.exit_code == 0
        # "Stopped." is written via info() → console.print, may not be in captured
        # output due to Rich console buffering; assert clean exit is sufficient
        assert result.exception is None or isinstance(result.exception, SystemExit)

    def test_runs_report_then_sleeps(self, state_dir, populated_state):
        calls = []

        def fake_sleep(n):
            calls.append(n)
            raise KeyboardInterrupt  # stop after first loop

        with patch("cli.main.report_health"), \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.dns_mgr") as mock_dns, \
             patch("cli.main.time") as mock_time:
            mock_wg.get_status.return_value = {"active": True, "peers": 0, "latest_handshake": None}
            mock_rat.is_running.return_value = True
            mock_rat.is_connected.return_value = True
            mock_dns.is_running.return_value = True
            mock_time.sleep.side_effect = fake_sleep
            result = runner.invoke(app, ["health", "--interval", "60"])
        assert result.exit_code == 0
        assert calls == [60]


# ---------------------------------------------------------------------------
# restart
# ---------------------------------------------------------------------------


class TestRestart:
    def test_restart_stops_running_services_then_starts(self, state_dir, populated_state):
        api_mock = _mock_api()
        with patch("cli.main._api", return_value=api_mock), \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat:
            mock_rat.is_running.return_value = True
            mock_rat.stop.return_value = True
            mock_rat.start.return_value = 8888
            mock_wg.up.return_value = None
            mock_wg.down.return_value = None
            result = runner.invoke(app, ["restart", "beacon-id-001"])
        assert result.exit_code == 0
        mock_rat.stop.assert_called()
        mock_wg.down.assert_called()
        mock_rat.start.assert_called()

    def test_restart_with_no_running_services_just_starts(self, state_dir, populated_state):
        api_mock = _mock_api()
        with patch("cli.main._api", return_value=api_mock), \
             patch("cli.main.wg") as mock_wg, \
             patch("cli.main.rathole_mgr") as mock_rat:
            mock_rat.is_running.return_value = False
            mock_rat.start.return_value = 9999
            mock_wg.up.return_value = None
            result = runner.invoke(app, ["restart", "beacon-id-001"])
        assert result.exit_code == 0
        mock_rat.stop.assert_not_called()


# ---------------------------------------------------------------------------
# stop - WireGuard down failure (warning, not abort)
# ---------------------------------------------------------------------------


class TestStopEdgeCases:
    def test_wg_down_failure_shows_warning_but_succeeds(self, state_dir, populated_state):
        with patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.wg") as mock_wg:
            mock_rat.stop.return_value = True
            mock_wg.down.side_effect = BeaconSystemError("interface not found")
            result = runner.invoke(app, ["stop", "beacon-id-001"])
        assert result.exit_code == 0
        assert "failed" in result.output.lower() or "WireGuard" in result.output


# ---------------------------------------------------------------------------
# delete - with running local services
# ---------------------------------------------------------------------------


class TestDeleteWithServices:
    def test_delete_stops_running_rathole(self, state_dir, populated_state):
        api_mock = MagicMock()
        api_mock.__enter__ = lambda s: api_mock
        api_mock.__exit__ = MagicMock(return_value=False)
        api_mock.delete.return_value = None

        with patch("cli.main._api", return_value=api_mock), \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.wg") as mock_wg:
            mock_rat.is_running.return_value = True
            mock_rat.stop.return_value = True
            mock_wg.down.return_value = None
            mock_wg.remove_config.return_value = None
            result = runner.invoke(app, ["delete", "beacon-id-001", "--yes"])
        assert result.exit_code == 0
        mock_rat.stop.assert_called_once_with("beacon-id-001")

    def test_delete_calls_wg_down(self, state_dir, populated_state):
        api_mock = MagicMock()
        api_mock.__enter__ = lambda s: api_mock
        api_mock.__exit__ = MagicMock(return_value=False)
        api_mock.delete.return_value = None

        with patch("cli.main._api", return_value=api_mock), \
             patch("cli.main.rathole_mgr") as mock_rat, \
             patch("cli.main.wg") as mock_wg:
            mock_rat.is_running.return_value = False
            mock_wg.down.return_value = None
            mock_wg.remove_config.return_value = None
            result = runner.invoke(app, ["delete", "beacon-id-001", "--yes"])
        assert result.exit_code == 0
        mock_wg.down.assert_called_once()
        mock_wg.remove_config.assert_called_once()


# ---------------------------------------------------------------------------
# update - cidrs and dns branches
# ---------------------------------------------------------------------------


class TestUpdateAllFields:
    def test_update_cidrs(self, state_dir):
        api_mock = MagicMock()
        api_mock.__enter__ = lambda s: api_mock
        api_mock.__exit__ = MagicMock(return_value=False)
        api_mock.update.return_value = {**SAMPLE_BEACON, "allowed_cidrs": ["10.0.0.0/8"]}

        with patch("cli.main._api", return_value=api_mock):
            result = runner.invoke(app, [
                "update", "beacon-id-001",
                "--cidrs", "10.0.0.0/8,172.16.0.0/12",
            ])
        assert result.exit_code == 0
        call_kwargs = api_mock.update.call_args
        assert "allowed_cidrs" in (call_kwargs.kwargs or call_kwargs[1])

    def test_update_api_error_exits(self, state_dir):
        api_mock = MagicMock()
        api_mock.__enter__ = lambda s: api_mock
        api_mock.__exit__ = MagicMock(return_value=False)
        api_mock.update.side_effect = BeaconAPIError("conflict", 409)

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


# ---------------------------------------------------------------------------
# list - pagination message
# ---------------------------------------------------------------------------


class TestListPagination:
    def test_shows_pagination_hint_when_more_pages(self, state_dir):
        api_mock = MagicMock()
        api_mock.__enter__ = lambda s: api_mock
        api_mock.__exit__ = MagicMock(return_value=False)
        # 50 results but per_page=20 → should show pagination hint
        api_mock.list.return_value = {
            "count": 50,
            "results": [SAMPLE_BEACON] * 20,
        }
        with patch("cli.main._api", return_value=api_mock):
            result = runner.invoke(app, ["list"])
        assert result.exit_code == 0
        assert "--page" in result.output


# ---------------------------------------------------------------------------
# test_connection edge cases
# ---------------------------------------------------------------------------


# TODO @0xacb deactivated for now (test command disabled)
# class TestTestConnectionEdgeCases:
#     def test_reachable_without_latency(self, state_dir):
#         api_mock = MagicMock()
#         api_mock.__enter__ = lambda s: api_mock
#         api_mock.__exit__ = MagicMock(return_value=False)
#         api_mock.test_connection.return_value = {"reachable": True}
#
#         with patch("cli.main._api", return_value=api_mock):
#             result = runner.invoke(app, ["test", "beacon-id-001", "10.0.0.1"])
#         assert result.exit_code == 0
#         assert "reachable" in result.output.lower()
#
#     def test_with_detail_message(self, state_dir):
#         api_mock = MagicMock()
#         api_mock.__enter__ = lambda s: api_mock
#         api_mock.__exit__ = MagicMock(return_value=False)
#         api_mock.test_connection.return_value = {
#             "reachable": False,
#             "detail": "Host is down",
#         }
#         with patch("cli.main._api", return_value=api_mock):
#             result = runner.invoke(app, ["test", "beacon-id-001", "10.0.0.1"])
#         assert result.exit_code == 0
#         assert "Host is down" in result.output
#
#     def test_api_error_exits(self, state_dir):
#         api_mock = MagicMock()
#         api_mock.__enter__ = lambda s: api_mock
#         api_mock.__exit__ = MagicMock(return_value=False)
#         api_mock.test_connection.side_effect = BeaconAPIError("gateway error", 502)
#
#         with patch("cli.main._api", return_value=api_mock):
#             result = runner.invoke(app, ["test", "beacon-id-001", "10.0.0.1"])
#         assert result.exit_code == 1
