"""Tests for dns.py - dnsmasq lifecycle management."""

from __future__ import annotations

import os
import signal
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from cli import dns as dns_mgr
from cli.exceptions import BeaconSystemError


# ---------------------------------------------------------------------------
# get_system_dns
# ---------------------------------------------------------------------------


class TestGetSystemDns:
    def test_reads_nameservers_from_resolv_conf(self, tmp_path):
        resolv = tmp_path / "resolv.conf"
        resolv.write_text("nameserver 8.8.8.8\nnameserver 1.1.1.1\n")
        with patch("builtins.open", return_value=resolv.open()):
            result = dns_mgr.get_system_dns()
        assert "8.8.8.8" in result
        assert "1.1.1.1" in result

    def test_returns_empty_list_on_oserror(self):
        with patch("builtins.open", side_effect=OSError("no file")):
            result = dns_mgr.get_system_dns()
        assert result == []

    def test_skips_lines_without_nameserver_keyword(self, tmp_path):
        resolv = tmp_path / "resolv.conf"
        resolv.write_text("search example.com\ndomain local\nnameserver 9.9.9.9\n")
        with patch("builtins.open", return_value=resolv.open()):
            result = dns_mgr.get_system_dns()
        assert result == ["9.9.9.9"]

    def test_skips_malformed_nameserver_lines(self, tmp_path):
        resolv = tmp_path / "resolv.conf"
        # "nameserver" with no IP (only one part after split)
        resolv.write_text("nameserver\nnameserver 1.2.3.4\n")
        with patch("builtins.open", return_value=resolv.open()):
            result = dns_mgr.get_system_dns()
        assert result == ["1.2.3.4"]


# ---------------------------------------------------------------------------
# is_installed
# ---------------------------------------------------------------------------


class TestIsInstalled:
    def test_returns_true_when_dnsmasq_found(self):
        with patch("subprocess.check_output", return_value=b"/usr/bin/dnsmasq"):
            assert dns_mgr.is_installed() is True

    def test_returns_false_when_dnsmasq_not_found(self):
        import subprocess
        with patch(
            "subprocess.check_output",
            side_effect=subprocess.CalledProcessError(1, "which"),
        ):
            assert dns_mgr.is_installed() is False

    def test_returns_false_when_which_not_found(self):
        with patch("subprocess.check_output", side_effect=FileNotFoundError):
            assert dns_mgr.is_installed() is False


# ---------------------------------------------------------------------------
# start
# ---------------------------------------------------------------------------


class TestStart:
    def test_returns_immediately_if_already_running(self, state_dir):
        pid_file = state_dir / "dns" / "beacon-1.pid"
        pid_file.parent.mkdir(parents=True, exist_ok=True)
        pid_file.write_text("12345")

        with patch("cli.dns.DNS_DIR", state_dir / "dns"), \
             patch("cli.dns.is_running", return_value=True), \
             patch("subprocess.run") as mock_run:
            dns_mgr.start("beacon-1", "10.0.0.1")
        mock_run.assert_not_called()

    def test_raises_when_no_upstream_dns(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"), \
             patch("cli.dns.is_running", return_value=False), \
             patch("cli.dns.get_system_dns", return_value=[]):
            with pytest.raises(BeaconSystemError, match="No nameservers"):
                dns_mgr.start("beacon-2", "10.0.0.2")

    def test_starts_dnsmasq_with_upstream_servers(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"), \
             patch("cli.dns.is_running", return_value=False), \
             patch("cli.dns.get_system_dns", return_value=["8.8.8.8", "1.1.1.1"]), \
             patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)
            dns_mgr.start("beacon-3", "10.0.0.3")
        call_args = mock_run.call_args[0][0]
        assert "dnsmasq" in call_args
        assert "--server=8.8.8.8" in call_args
        assert "--server=1.1.1.1" in call_args

    def test_raises_when_dnsmasq_fails(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"), \
             patch("cli.dns.is_running", return_value=False), \
             patch("cli.dns.get_system_dns", return_value=["8.8.8.8"]), \
             patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1, stderr="permission denied", stdout="")
            with pytest.raises(BeaconSystemError, match="dnsmasq failed to start"):
                dns_mgr.start("beacon-4", "10.0.0.4")

    def test_raises_when_dnsmasq_not_found(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"), \
             patch("cli.dns.is_running", return_value=False), \
             patch("cli.dns.get_system_dns", return_value=["8.8.8.8"]), \
             patch("subprocess.run", side_effect=FileNotFoundError):
            with pytest.raises(BeaconSystemError, match="dnsmasq not found"):
                dns_mgr.start("beacon-5", "10.0.0.5")


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


class TestStop:
    def test_returns_false_when_no_pid_file(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"):
            result = dns_mgr.stop("no-pid-beacon")
        assert result is False

    def test_sends_sigterm_and_returns_true(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-stop.pid"
        pid_file.write_text("22222")

        with patch("cli.dns.DNS_DIR", dns_dir), \
             patch("os.kill") as mock_kill:
            result = dns_mgr.stop("beacon-stop")
        assert result is True
        mock_kill.assert_called_once_with(22222, signal.SIGTERM)
        assert not pid_file.exists()

    def test_returns_false_when_process_not_found(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-dead.pid"
        pid_file.write_text("33333")

        with patch("cli.dns.DNS_DIR", dns_dir), \
             patch("os.kill", side_effect=ProcessLookupError):
            result = dns_mgr.stop("beacon-dead")
        assert result is False
        # pid file should be cleaned up
        assert not pid_file.exists()

    def test_returns_false_on_invalid_pid_content(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-bad.pid"
        pid_file.write_text("not-a-number")

        with patch("cli.dns.DNS_DIR", dns_dir):
            result = dns_mgr.stop("beacon-bad")
        assert result is False


# ---------------------------------------------------------------------------
# is_running
# ---------------------------------------------------------------------------


class TestIsRunning:
    def test_returns_false_when_no_pid_file(self, state_dir):
        with patch("cli.dns.DNS_DIR", state_dir / "dns"):
            assert dns_mgr.is_running("no-such-beacon") is False

    def test_returns_true_when_process_alive(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-alive.pid"
        pid_file.write_text("44444")

        with patch("cli.dns.DNS_DIR", dns_dir), \
             patch("os.kill", return_value=None):  # signal 0 succeeds
            assert dns_mgr.is_running("beacon-alive") is True

    def test_returns_false_when_process_not_running(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-gone.pid"
        pid_file.write_text("55555")

        with patch("cli.dns.DNS_DIR", dns_dir), \
             patch("os.kill", side_effect=ProcessLookupError):
            assert dns_mgr.is_running("beacon-gone") is False

    def test_returns_false_on_bad_pid_file(self, state_dir):
        dns_dir = state_dir / "dns"
        dns_dir.mkdir(parents=True, exist_ok=True)
        pid_file = dns_dir / "beacon-bad.pid"
        pid_file.write_text("garbage")

        with patch("cli.dns.DNS_DIR", dns_dir):
            assert dns_mgr.is_running("beacon-bad") is False
