"""Unit tests for infrastructure.nostr.identity."""

from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from infrastructure.nostr.identity import AnnounceResult, NostrIdentityManager
from infrastructure.nostr.keypair import generate_keypair


@pytest.fixture()
def manager():
    return NostrIdentityManager()


@pytest.fixture()
def kp():
    return generate_keypair()


class TestAnnounceResult:
    def test_any_relay_ok_false_when_empty(self):
        r = AnnounceResult()
        assert r.any_relay_ok is False

    def test_any_relay_ok_true_when_one_ok(self):
        r = AnnounceResult(relay_results={"wss://a": True, "wss://b": False})
        assert r.any_relay_ok is True

    def test_to_dict_keys(self):
        r = AnnounceResult(kind_0_ok=True, relay_results={"wss://a": True})
        d = r.to_dict()
        assert set(d) == {"kind_0", "kind_31990", "relays"}


class TestGetKeypair:
    def test_returns_none_when_no_privkey(self, manager):
        mock_settings = MagicMock(nostr_privkey="")
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            assert manager.get_keypair() is None

    def test_returns_keypair_when_configured(self, manager, kp):
        mock_settings = MagicMock(nostr_privkey=kp.privkey_hex)
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            result = manager.get_keypair()
        assert result is not None
        assert result.pubkey_hex == kp.pubkey_hex

    def test_returns_none_on_invalid_key(self, manager):
        mock_settings = MagicMock(nostr_privkey="not_a_valid_key")
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            assert manager.get_keypair() is None


class TestGetRelayUrls:
    def test_empty_string_returns_empty_list(self, manager):
        mock_settings = MagicMock(nostr_relays="")
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            assert manager.get_relay_urls() == []

    def test_single_relay(self, manager):
        mock_settings = MagicMock(nostr_relays="wss://relay.damus.io")
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            urls = manager.get_relay_urls()
        assert urls == ["wss://relay.damus.io"]

    def test_multiple_relays(self, manager):
        mock_settings = MagicMock(nostr_relays="wss://a.com,wss://b.com, wss://c.com ")
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            urls = manager.get_relay_urls()
        assert urls == ["wss://a.com", "wss://b.com", "wss://c.com"]


class TestBuildProfileEvent:
    def test_kind_is_0(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_profile_event(kp)
        assert ev["kind"] == 0

    def test_content_contains_name(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="A great AI agent",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_profile_event(kp)
        profile = json.loads(ev["content"])
        assert profile["name"] == "Timmy"

    def test_nip05_included_when_set(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="timmy@tower.local",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_profile_event(kp)
        profile = json.loads(ev["content"])
        assert profile["nip05"] == "timmy@tower.local"

    def test_nip05_omitted_when_empty(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_profile_event(kp)
        profile = json.loads(ev["content"])
        assert "nip05" not in profile

    def test_default_name_when_blank(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_profile_event(kp)
        profile = json.loads(ev["content"])
        assert profile["name"] == "Timmy"  # default


class TestBuildCapabilityEvent:
    def test_kind_is_31990(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_capability_event(kp)
        assert ev["kind"] == 31990

    def test_has_d_tag(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_capability_event(kp)
        d_tags = [t for t in ev["tags"] if t[0] == "d"]
        assert d_tags
        assert d_tags[0][1] == "timmy-mission-control"

    def test_content_is_json(self, manager, kp):
        mock_settings = MagicMock(
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            ev = manager.build_capability_event(kp)
        parsed = json.loads(ev["content"])
        assert "name" in parsed
        assert "capabilities" in parsed


class TestAnnounce:
    @pytest.mark.asyncio
    async def test_returns_empty_result_when_no_privkey(self, manager):
        mock_settings = MagicMock(
            nostr_privkey="",
            nostr_relays="",
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            result = await manager.announce()
        assert result.kind_0_ok is False
        assert result.kind_31990_ok is False

    @pytest.mark.asyncio
    async def test_returns_empty_result_when_no_relays(self, manager, kp):
        mock_settings = MagicMock(
            nostr_privkey=kp.privkey_hex,
            nostr_relays="",
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with patch("infrastructure.nostr.identity.settings", mock_settings):
            result = await manager.announce()
        assert result.kind_0_ok is False

    @pytest.mark.asyncio
    async def test_publishes_kind0_and_kind31990(self, manager, kp):
        mock_settings = MagicMock(
            nostr_privkey=kp.privkey_hex,
            nostr_relays="wss://relay.test",
            nostr_profile_name="Timmy",
            nostr_profile_about="Test agent",
            nostr_profile_picture="",
            nostr_nip05="timmy@test",
        )
        with (
            patch("infrastructure.nostr.identity.settings", mock_settings),
            patch(
                "infrastructure.nostr.identity.publish_to_relays",
                new=AsyncMock(return_value={"wss://relay.test": True}),
            ) as mock_publish,
        ):
            result = await manager.announce()

        assert mock_publish.call_count == 2  # kind 0 + kind 31990
        assert result.kind_0_ok is True
        assert result.kind_31990_ok is True
        assert result.relay_results["wss://relay.test"] is True

    @pytest.mark.asyncio
    async def test_degrades_gracefully_on_relay_failure(self, manager, kp):
        mock_settings = MagicMock(
            nostr_privkey=kp.privkey_hex,
            nostr_relays="wss://relay.test",
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with (
            patch("infrastructure.nostr.identity.settings", mock_settings),
            patch(
                "infrastructure.nostr.identity.publish_to_relays",
                new=AsyncMock(return_value={"wss://relay.test": False}),
            ),
        ):
            result = await manager.announce()

        assert result.kind_0_ok is False
        assert result.kind_31990_ok is False

    @pytest.mark.asyncio
    async def test_never_raises_on_exception(self, manager, kp):
        mock_settings = MagicMock(
            nostr_privkey=kp.privkey_hex,
            nostr_relays="wss://relay.test",
            nostr_profile_name="Timmy",
            nostr_profile_about="",
            nostr_profile_picture="",
            nostr_nip05="",
        )
        with (
            patch("infrastructure.nostr.identity.settings", mock_settings),
            patch(
                "infrastructure.nostr.identity.publish_to_relays",
                new=AsyncMock(side_effect=Exception("relay exploded")),
            ),
        ):
            # Must not raise
            result = await manager.announce()
        assert isinstance(result, AnnounceResult)
