# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Licensed under the MIT License

"""Tests for Tor hidden service transport — all mocked, no real Tor needed."""

import json
from unittest.mock import patch

import pytest


class TestTorTransportAvailability:
    """Test is_available() returns correct structure."""

    def test_is_available_returns_dict(self):
        from familiar.core.mesh.tor_transport import TorTransport

        result = TorTransport.is_available()
        assert isinstance(result, dict)
        assert "available" in result
        assert "tor_binary" in result
        assert "stem_library" in result
        assert "socks_library" in result
        assert "missing" in result
        assert isinstance(result["missing"], list)

    def test_is_available_missing_has_entries(self):
        """When deps are missing, missing list is non-empty."""
        from familiar.core.mesh.tor_transport import (
            HAS_SOCKS,
            HAS_STEM,
            HAS_TOR_BINARY,
            TorTransport,
        )

        result = TorTransport.is_available()
        expected_missing = 0
        if not HAS_TOR_BINARY:
            expected_missing += 1
        if not HAS_STEM:
            expected_missing += 1
        if not HAS_SOCKS:
            expected_missing += 1
        assert len(result["missing"]) == expected_missing

    def test_available_bool_matches_flags(self):
        from familiar.core.mesh.tor_transport import (
            HAS_SOCKS,
            HAS_STEM,
            HAS_TOR_BINARY,
            TorTransport,
        )

        result = TorTransport.is_available()
        assert result["available"] == (HAS_TOR_BINARY and HAS_STEM and HAS_SOCKS)


class TestTorState:
    """Test TorState dataclass."""

    def test_defaults(self):
        from familiar.core.mesh.tor_transport import TorState

        state = TorState()
        assert state.onion_address == ""
        assert state.private_key_type == ""
        assert state.private_key == ""
        assert state.created_at == ""
        assert state.last_started == ""

    def test_to_dict(self):
        from familiar.core.mesh.tor_transport import TorState

        state = TorState(onion_address="test.onion", created_at="2026-01-01")
        d = state.to_dict()
        assert d["onion_address"] == "test.onion"
        assert d["created_at"] == "2026-01-01"
        assert d["private_key"] == ""

    def test_from_dict(self):
        from familiar.core.mesh.tor_transport import TorState

        d = {"onion_address": "abc.onion", "private_key": "secret", "extra_field": "ignored"}
        state = TorState.from_dict(d)
        assert state.onion_address == "abc.onion"
        assert state.private_key == "secret"

    def test_roundtrip(self):
        from familiar.core.mesh.tor_transport import TorState

        original = TorState(
            onion_address="test.onion",
            private_key_type="ED25519-V3",
            private_key="key123",
            created_at="2026-01-01T00:00:00",
            last_started="2026-03-07T12:00:00",
        )
        restored = TorState.from_dict(original.to_dict())
        assert restored.onion_address == original.onion_address
        assert restored.private_key == original.private_key


class TestTorTransportInit:
    """Test TorTransport initialization without running Tor."""

    def test_init_defaults(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        t = TorTransport(tor_dir=tmp_path / "tor")
        assert t.control_port == 9051
        assert t.socks_port == 9050
        assert t.is_running is False
        assert t.onion_address == ""

    def test_init_custom_ports(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        t = TorTransport(control_port=9151, socks_port=9150, tor_dir=tmp_path / "tor")
        assert t.control_port == 9151
        assert t.socks_port == 9150

    def test_get_status_not_running(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        t = TorTransport(tor_dir=tmp_path / "tor")
        status = t.get_status()
        assert status["running"] is False
        assert status["onion_address"] == ""
        assert "has_stem" in status
        assert "has_socks" in status
        assert "has_tor_binary" in status

    def test_tor_dir_created(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        tor_dir = tmp_path / "tor"
        assert not tor_dir.exists()
        TorTransport(tor_dir=tor_dir)
        assert tor_dir.exists()


class TestTorTransportStatePersistence:
    """Test state save/load."""

    def test_save_state_writes_json(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorState, TorTransport

        tor_dir = tmp_path / "tor"
        t = TorTransport(tor_dir=tor_dir)
        t._state = TorState(onion_address="save_test.onion", created_at="2026-01-01")
        t._save_state()

        state_file = tor_dir / "tor_state.json"
        assert state_file.exists()
        data = json.loads(state_file.read_text())
        assert data["onion_address"] == "save_test.onion"
        assert data["created_at"] == "2026-01-01"

    def test_load_state_reads_json(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        tor_dir = tmp_path / "tor"
        tor_dir.mkdir(parents=True)
        state_file = tor_dir / "tor_state.json"
        state_file.write_text(
            json.dumps({"onion_address": "loaded.onion", "private_key": "pk123"})
        )

        # Patch the module-level TOR_STATE_FILE to point to our temp file
        with patch("familiar.core.mesh.tor_transport.TOR_STATE_FILE", state_file):
            t = TorTransport(tor_dir=tor_dir)
            assert t._state.onion_address == "loaded.onion"
            assert t._state.private_key == "pk123"

    def test_load_state_handles_corrupt_json(self, tmp_path):
        from familiar.core.mesh.tor_transport import TorTransport

        tor_dir = tmp_path / "tor"
        tor_dir.mkdir(parents=True)
        state_file = tor_dir / "tor_state.json"
        state_file.write_text("not valid json{{{")

        with patch("familiar.core.mesh.tor_transport.TOR_STATE_FILE", state_file):
            t = TorTransport(tor_dir=tor_dir)
            assert t._state.onion_address == ""  # Falls back to defaults


class TestTorTransportStartStop:
    """Test start/stop with mocked stem."""

    @pytest.mark.asyncio
    async def test_start_without_stem(self, tmp_path):
        """Start returns False when stem is not available."""
        from familiar.core.mesh.tor_transport import TorTransport

        with patch("familiar.core.mesh.tor_transport.HAS_STEM", False):
            t = TorTransport(tor_dir=tmp_path / "tor")
            result = await t.start(gateway_port=18789)
            assert result is False
            assert t.is_running is False

    @pytest.mark.asyncio
    async def test_start_without_tor_binary(self, tmp_path):
        """Start returns False when tor binary is not found."""
        from familiar.core.mesh.tor_transport import TorTransport

        with patch("familiar.core.mesh.tor_transport.HAS_STEM", True), patch(
            "familiar.core.mesh.tor_transport.HAS_TOR_BINARY", False
        ):
            t = TorTransport(tor_dir=tmp_path / "tor")
            result = await t.start(gateway_port=18789)
            assert result is False

    @pytest.mark.asyncio
    async def test_stop_when_not_running(self, tmp_path):
        """Stop is a no-op when not running."""
        from familiar.core.mesh.tor_transport import TorTransport

        t = TorTransport(tor_dir=tmp_path / "tor")
        await t.stop()  # Should not raise
        assert t.is_running is False

    @pytest.mark.asyncio
    async def test_connect_to_onion_without_socks(self, tmp_path):
        """connect_to_onion returns None when python-socks is missing."""
        from familiar.core.mesh.tor_transport import TorTransport

        with patch("familiar.core.mesh.tor_transport.HAS_SOCKS", False):
            t = TorTransport(tor_dir=tmp_path / "tor")
            result = await t.connect_to_onion("test.onion", 18789)
            assert result is None


class TestTorExports:
    """Test that TorTransport and TorState are exported from familiar.core.mesh."""

    def test_tor_transport_exported(self):
        from familiar.core.mesh import TorTransport

        assert TorTransport is not None

    def test_tor_state_exported(self):
        from familiar.core.mesh import TorState

        assert TorState is not None

    def test_in_all(self):
        import familiar.core.mesh as mesh

        assert "TorTransport" in mesh.__all__
        assert "TorState" in mesh.__all__
