"""Tests for symbol resolution: alias index, resolver, and ambiguity handling.

Validates Requirements 5.2, 5.3, 5.4, 5.5, 5.6
"""

from __future__ import annotations

import pytest

from services.intelligence_pipeline_v3.resolution.alias_index import (
    AliasIndex,
    IndexEntry,
    build_alias_index,
)
from services.intelligence_pipeline_v3.resolution.explicit_vs_inferred import (
    ClassifiedMentionType,
    classify_mention,
    to_mention_type,
)
from services.intelligence_pipeline_v3.resolution.models import (
    MatchType,
    MentionType,
    ResolutionCandidate,
    UnresolvedMention,
    UnresolvedReason,
)
from services.intelligence_pipeline_v3.resolution.symbol_resolver import SymbolResolver

# --- Fixtures ---


def _sample_companies() -> list[dict]:
    """Standard set of test companies matching the seed structure."""
    return [
        {
            "id": "11111111-1111-1111-1111-111111111111",
            "ticker": "AAPL",
            "legal_name": "Apple Inc.",
            "aliases": [
                {"alias": "Apple", "alias_type": "brand"},
                {"alias": "iPhone", "alias_type": "product"},
            ],
        },
        {
            "id": "22222222-2222-2222-2222-222222222222",
            "ticker": "GOOGL",
            "legal_name": "Alphabet Inc.",
            "aliases": [
                {"alias": "Google", "alias_type": "brand"},
                {"alias": "Alphabet", "alias_type": "legal_name"},
                {"alias": "YouTube", "alias_type": "product"},
            ],
        },
        {
            "id": "33333333-3333-3333-3333-333333333333",
            "ticker": "MSFT",
            "legal_name": "Microsoft Corporation",
            "aliases": [
                {"alias": "Microsoft", "alias_type": "brand"},
                {"alias": "Azure", "alias_type": "product"},
                {"alias": "Windows", "alias_type": "product"},
            ],
        },
        {
            "id": "44444444-4444-4444-4444-444444444444",
            "ticker": "META",
            "legal_name": "Meta Platforms Inc.",
            "aliases": [
                {"alias": "Facebook", "alias_type": "brand"},
                {"alias": "Instagram", "alias_type": "product"},
                {"alias": "WhatsApp", "alias_type": "product"},
            ],
        },
        {
            "id": "55555555-5555-5555-5555-555555555555",
            "ticker": "JPM",
            "legal_name": "JPMorgan Chase & Co.",
            "aliases": [
                {"alias": "JPMorgan", "alias_type": "brand"},
                {"alias": "Chase", "alias_type": "brand"},
            ],
        },
        {
            "id": "66666666-6666-6666-6666-666666666666",
            "ticker": "V",
            "legal_name": "Visa Inc.",
            "aliases": [
                {"alias": "Visa", "alias_type": "brand"},
            ],
        },
    ]


def _companies_with_shared_alias() -> list[dict]:
    """Companies that share an alias, creating ambiguity."""
    return [
        {
            "id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
            "ticker": "CARR",
            "legal_name": "Carrier Global Corporation",
            "aliases": [
                {"alias": "Carrier", "alias_type": "brand"},
            ],
        },
        {
            "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
            "ticker": "CSX",
            "legal_name": "CSX Corporation",
            "aliases": [
                {"alias": "Carrier", "alias_type": "brand"},  # shared alias!
            ],
        },
    ]


def _companies_with_multiple_shared_aliases() -> list[dict]:
    """Three companies sharing the 'Mercury' alias plus additional overlaps."""
    return [
        {
            "id": "aaa11111-1111-1111-1111-111111111111",
            "ticker": "MCY",
            "legal_name": "Mercury General Corporation",
            "aliases": [
                {"alias": "Mercury", "alias_type": "brand"},
                {"alias": "Mercury Insurance", "alias_type": "brand"},
            ],
        },
        {
            "id": "bbb22222-2222-2222-2222-222222222222",
            "ticker": "MRCY",
            "legal_name": "Mercury Systems Inc.",
            "aliases": [
                {"alias": "Mercury", "alias_type": "brand"},
                {"alias": "Mercury Systems", "alias_type": "brand"},
            ],
        },
        {
            "id": "ccc33333-3333-3333-3333-333333333333",
            "ticker": "MERC",
            "legal_name": "Mercer International Inc.",
            "aliases": [
                {"alias": "Mercury", "alias_type": "brand"},  # 3-way shared
                {"alias": "Mercer", "alias_type": "brand"},
            ],
        },
    ]


@pytest.fixture
def resolver() -> SymbolResolver:
    """Create a resolver loaded with sample companies."""
    r = SymbolResolver()
    r.load_registry(_sample_companies())
    return r


@pytest.fixture
def ambiguous_resolver() -> SymbolResolver:
    """Create a resolver with companies sharing aliases."""
    r = SymbolResolver()
    r.load_registry(_companies_with_shared_alias())
    return r


@pytest.fixture
def multi_ambiguous_resolver() -> SymbolResolver:
    """Create a resolver with three companies sharing one alias."""
    r = SymbolResolver()
    r.load_registry(_companies_with_multiple_shared_aliases())
    return r


# --- Tests: build_alias_index (Task 24.1) ---


class TestBuildAliasIndex:
    """Test the build_alias_index factory function."""

    def test_builds_from_company_list(self) -> None:
        companies = _sample_companies()
        index = build_alias_index(companies)
        assert len(index) > 0

    def test_indexes_tickers(self) -> None:
        companies = _sample_companies()
        index = build_alias_index(companies)
        entries = index.lookup("AAPL")
        assert len(entries) == 1
        assert entries[0].ticker == "AAPL"
        assert entries[0].match_type == "exact_ticker"

    def test_indexes_legal_names(self) -> None:
        companies = _sample_companies()
        index = build_alias_index(companies)
        entries = index.lookup("Microsoft Corporation")
        assert len(entries) >= 1
        assert any(e.ticker == "MSFT" for e in entries)

    def test_indexes_aliases(self) -> None:
        companies = _sample_companies()
        index = build_alias_index(companies)
        entries = index.lookup("Google")
        assert len(entries) == 1
        assert entries[0].ticker == "GOOGL"
        assert entries[0].match_type == "alias"

    def test_handles_tuple_aliases(self) -> None:
        companies = [
            {
                "id": "99999999-9999-9999-9999-999999999999",
                "ticker": "TEST",
                "legal_name": "Test Corp.",
                "aliases": [("TestAlias", "brand"), ("AnotherAlias", "product")],
            }
        ]
        index = build_alias_index(companies)
        entries = index.lookup("TestAlias")
        assert len(entries) == 1
        assert entries[0].ticker == "TEST"

    def test_handles_empty_company_list(self) -> None:
        index = build_alias_index([])
        assert len(index) == 0

    def test_handles_company_without_aliases(self) -> None:
        companies = [
            {
                "id": "88888888-8888-8888-8888-888888888888",
                "ticker": "BARE",
                "legal_name": "Bare Corp.",
            }
        ]
        index = build_alias_index(companies)
        entries = index.lookup("BARE")
        # "BARE" matches both as ticker and normalized legal name "Bare Corp." → "bare"
        assert len(entries) >= 1
        assert all(e.company_id == "88888888-8888-8888-8888-888888888888" for e in entries)

    def test_case_insensitive_lookup(self) -> None:
        companies = _sample_companies()
        index = build_alias_index(companies)
        entries_upper = index.lookup("AAPL")
        entries_lower = index.lookup("aapl")
        assert len(entries_upper) == len(entries_lower)


# --- Tests: Exact Ticker Match (Task 24.2) ---


class TestExactTickerMatch:
    """Test resolving by ticker symbol."""

    def test_aapl_resolves(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("AAPL")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"
        assert result.candidates[0].company_id == "11111111-1111-1111-1111-111111111111"
        assert result.candidates[0].match_type == MatchType.exact_ticker
        assert result.candidates[0].confidence >= 0.9
        assert not result.is_ambiguous

    def test_googl_resolves(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("GOOGL")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "GOOGL"
        assert result.candidates[0].match_type == MatchType.exact_ticker

    def test_single_char_ticker(self, resolver: SymbolResolver) -> None:
        """Ticker 'V' (Visa) should resolve correctly."""
        result = resolver.resolve("V")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "V"
        assert result.candidates[0].name == "Visa Inc."

    def test_ambiguity_margin_is_1_for_single_match(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("AAPL")
        assert result.ambiguity_margin == 1.0


# --- Tests: Ranked Candidates and Ambiguity Margins (Task 24.2) ---


class TestRankedCandidates:
    """Test that candidates are ranked by confidence with proper margins."""

    def test_candidates_sorted_descending(self, ambiguous_resolver: SymbolResolver) -> None:
        result = ambiguous_resolver.resolve("Carrier")
        # Multiple candidates should be sorted by confidence descending
        for i in range(len(result.candidates) - 1):
            assert result.candidates[i].confidence >= result.candidates[i + 1].confidence

    def test_ambiguity_margin_calculated(self, ambiguous_resolver: SymbolResolver) -> None:
        result = ambiguous_resolver.resolve("Carrier")
        # Both are alias matches → same confidence → margin = 0
        assert result.ambiguity_margin == pytest.approx(0.0)

    def test_unambiguous_has_high_margin(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("AAPL")
        assert result.ambiguity_margin == 1.0
        assert not result.is_ambiguous

    def test_three_way_ambiguity(self, multi_ambiguous_resolver: SymbolResolver) -> None:
        """Mercury alias shared by 3 companies → ambiguous with 3 candidates."""
        result = multi_ambiguous_resolver.resolve("Mercury")
        assert len(result.candidates) == 3
        assert result.is_ambiguous
        # All have same confidence (alias match), so margin = 0
        assert result.ambiguity_margin == pytest.approx(0.0)

    def test_unique_alias_not_ambiguous(self, multi_ambiguous_resolver: SymbolResolver) -> None:
        """Mercury Insurance is unique to MCY."""
        result = multi_ambiguous_resolver.resolve("Mercury Insurance")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MCY"
        assert not result.is_ambiguous


# --- Tests: Explicit vs Inferred Exposure (Task 24.3) ---


class TestMentionTypes:
    """Test separation of explicit mentions from inferred exposures."""

    def test_explicit_mention_default(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("AAPL")
        assert result.mention_type == MentionType.explicit

    def test_inferred_mention(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("AAPL", mention_type=MentionType.inferred)
        assert result.mention_type == MentionType.inferred
        assert len(result.candidates) == 1

    def test_inferred_preserves_candidates(self, resolver: SymbolResolver) -> None:
        explicit = resolver.resolve("Google", mention_type=MentionType.explicit)
        inferred = resolver.resolve("Google", mention_type=MentionType.inferred)
        # Same candidates, different mention_type
        assert len(explicit.candidates) == len(inferred.candidates)
        assert explicit.candidates[0].ticker == inferred.candidates[0].ticker
        assert explicit.mention_type == MentionType.explicit
        assert inferred.mention_type == MentionType.inferred

    def test_unresolved_preserves_mention_type(self, resolver: SymbolResolver) -> None:
        """Even empty results carry the mention_type."""
        result = resolver.resolve("UnknownCorp", mention_type=MentionType.inferred)
        assert result.mention_type == MentionType.inferred
        assert len(result.candidates) == 0

    def test_mention_type_enum_values(self) -> None:
        assert MentionType.explicit.value == "explicit"
        assert MentionType.inferred.value == "inferred"


# --- Tests: Unresolved Mentions Preserved (Task 24.4) ---


class TestUnresolvedMentions:
    """Test that unresolved mentions are preserved without invented tickers."""

    def test_unknown_company_returns_empty(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Palantir Technologies")
        assert len(result.candidates) == 0
        assert not result.is_ambiguous

    def test_unknown_returns_unresolved_mention(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve_or_unresolved(
            "Palantir Technologies", start_char=0, end_char=21
        )
        assert isinstance(result, UnresolvedMention)
        assert result.reason == UnresolvedReason.not_in_registry
        assert result.literal_text == "Palantir Technologies"

    def test_unresolved_preserves_offsets(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve_or_unresolved(
            "SomeUnknownCo", start_char=42, end_char=55
        )
        assert isinstance(result, UnresolvedMention)
        assert result.start_char == 42
        assert result.end_char == 55

    def test_no_ticker_invented_for_unknown(self, resolver: SymbolResolver) -> None:
        """The resolver MUST NOT invent a ticker for unresolved mentions."""
        result = resolver.resolve("Palantir Technologies")
        # No candidates means no ticker was invented
        assert len(result.candidates) == 0
        # Using resolve_or_unresolved, canonical_id equivalent is None (UnresolvedMention)
        unresolved = resolver.resolve_or_unresolved(
            "Palantir Technologies", start_char=0, end_char=21
        )
        assert isinstance(unresolved, UnresolvedMention)
        # Verify the literal text is preserved exactly as given
        assert unresolved.literal_text == "Palantir Technologies"

    def test_empty_mention_returns_empty(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("")
        assert len(result.candidates) == 0

    def test_gibberish_returns_empty(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("xyzzy123abc")
        assert len(result.candidates) == 0

    def test_ambiguous_marked_as_unresolved(self, ambiguous_resolver: SymbolResolver) -> None:
        """Ambiguous aliases return UnresolvedMention with reason=ambiguous."""
        result = ambiguous_resolver.resolve_or_unresolved(
            "Carrier", start_char=10, end_char=17
        )
        assert isinstance(result, UnresolvedMention)
        assert result.reason == UnresolvedReason.ambiguous
        assert result.literal_text == "Carrier"
        assert result.start_char == 10
        assert result.end_char == 17


# --- Tests: Aliases Shared by Multiple Companies (Task 24.5) ---


class TestSharedAliases:
    """Test behavior when aliases are shared by multiple companies."""

    def test_shared_alias_returns_multiple_candidates(
        self, ambiguous_resolver: SymbolResolver
    ) -> None:
        result = ambiguous_resolver.resolve("Carrier")
        assert len(result.candidates) == 2
        tickers = {c.ticker for c in result.candidates}
        assert "CARR" in tickers
        assert "CSX" in tickers

    def test_shared_alias_is_ambiguous(self, ambiguous_resolver: SymbolResolver) -> None:
        result = ambiguous_resolver.resolve("Carrier")
        # Both are alias matches with the same confidence, so margin = 0
        assert result.is_ambiguous
        assert result.ambiguity_margin < 0.15

    def test_three_companies_share_alias(
        self, multi_ambiguous_resolver: SymbolResolver
    ) -> None:
        """Three companies sharing 'Mercury' → all three returned as candidates."""
        result = multi_ambiguous_resolver.resolve("Mercury")
        assert len(result.candidates) == 3
        tickers = {c.ticker for c in result.candidates}
        assert "MCY" in tickers
        assert "MRCY" in tickers
        assert "MERC" in tickers

    def test_three_way_shared_alias_ambiguity_margin(
        self, multi_ambiguous_resolver: SymbolResolver
    ) -> None:
        """With 3 same-confidence candidates, margin between top-2 is 0."""
        result = multi_ambiguous_resolver.resolve("Mercury")
        assert result.ambiguity_margin == pytest.approx(0.0)
        assert result.is_ambiguous

    def test_unique_alias_among_shared(
        self, multi_ambiguous_resolver: SymbolResolver
    ) -> None:
        """'Mercury Systems' is unique to MRCY even though 'Mercury' is shared."""
        result = multi_ambiguous_resolver.resolve("Mercury Systems")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MRCY"
        assert not result.is_ambiguous
        assert result.ambiguity_margin == 1.0

    def test_shared_alias_all_candidates_have_scores(
        self, ambiguous_resolver: SymbolResolver
    ) -> None:
        """All candidates from a shared alias should have valid confidence scores."""
        result = ambiguous_resolver.resolve("Carrier")
        for candidate in result.candidates:
            assert 0.0 <= candidate.confidence <= 1.0
            assert candidate.match_type == MatchType.alias

    def test_shared_alias_companies_have_distinct_ids(
        self, ambiguous_resolver: SymbolResolver
    ) -> None:
        """Shared-alias candidates should have unique company_ids."""
        result = ambiguous_resolver.resolve("Carrier")
        company_ids = [c.company_id for c in result.candidates]
        assert len(company_ids) == len(set(company_ids))

    def test_ticker_not_shared_even_when_alias_is(
        self, ambiguous_resolver: SymbolResolver
    ) -> None:
        """Direct ticker lookup for CARR is unambiguous even if 'Carrier' is shared."""
        result = ambiguous_resolver.resolve("CARR")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "CARR"
        assert not result.is_ambiguous

    def test_shared_alias_resolve_or_unresolved_returns_unresolved(
        self, ambiguous_resolver: SymbolResolver
    ) -> None:
        """resolve_or_unresolved returns UnresolvedMention for ambiguous aliases."""
        result = ambiguous_resolver.resolve_or_unresolved(
            "Carrier", start_char=0, end_char=7
        )
        assert isinstance(result, UnresolvedMention)
        assert result.reason == UnresolvedReason.ambiguous


# --- Tests: Exact Name Match ---


class TestExactNameMatch:
    """Test resolving by full legal name."""

    def test_apple_inc(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Apple Inc.")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"
        assert result.candidates[0].match_type == MatchType.exact_name

    def test_alphabet_inc(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Alphabet Inc.")
        # "Alphabet Inc." → normalized "alphabet" matches the alias entry.
        assert len(result.candidates) >= 1
        assert result.candidates[0].ticker == "GOOGL"

    def test_microsoft_corporation(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Microsoft Corporation")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MSFT"


# --- Tests: Alias Match ---


class TestAliasMatch:
    """Test resolving by known alias."""

    def test_alphabet_alias(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Alphabet")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "GOOGL"

    def test_google_alias(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Google")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "GOOGL"
        assert result.candidates[0].match_type == MatchType.alias

    def test_facebook_alias(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Facebook")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "META"

    def test_iphone_product_alias(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("iPhone")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_chase_alias(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Chase")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "JPM"


# --- Tests: Case Insensitivity ---


class TestCaseInsensitivity:
    """Test that matching is case-insensitive."""

    def test_ticker_lowercase(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("aapl")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_ticker_mixed_case(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Aapl")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_name_uppercase(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("MICROSOFT CORPORATION")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MSFT"

    def test_alias_mixed_case(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("GOOGLE")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "GOOGL"

    def test_alias_all_lower(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("facebook")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "META"


# --- Tests: Suffix Variations ---


class TestSuffixVariations:
    """Test that corporate suffixes (Inc, Corp, LLC) are stripped during matching."""

    def test_without_inc(self, resolver: SymbolResolver) -> None:
        """'Apple' without 'Inc.' should still match via alias."""
        result = resolver.resolve("Apple")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_with_inc_period(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Apple Inc.")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_with_incorporated(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Apple Incorporated")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "AAPL"

    def test_corp_stripped(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Microsoft Corp.")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MSFT"

    def test_corp_full(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Microsoft Corp")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MSFT"

    def test_corporation_stripped(self, resolver: SymbolResolver) -> None:
        result = resolver.resolve("Microsoft Corporation")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "MSFT"

    def test_llc_suffix(self) -> None:
        """Test that LLC suffix is stripped."""
        resolver = SymbolResolver()
        resolver.load_registry([
            {
                "id": "77777777-7777-7777-7777-777777777777",
                "ticker": "TEST",
                "legal_name": "TestCo LLC",
                "aliases": [],
            }
        ])
        result = resolver.resolve("TestCo")
        assert len(result.candidates) == 1
        assert result.candidates[0].ticker == "TEST"


# --- Tests: AliasIndex Directly ---


class TestAliasIndex:
    """Unit tests for the AliasIndex component."""

    def test_normalize_strips_inc(self) -> None:
        assert AliasIndex.normalize("Apple Inc.") == "apple"

    def test_normalize_strips_corporation(self) -> None:
        assert AliasIndex.normalize("Microsoft Corporation") == "microsoft"

    def test_normalize_strips_llc(self) -> None:
        assert AliasIndex.normalize("SomeCompany LLC") == "somecompany"

    def test_normalize_strips_limited(self) -> None:
        assert AliasIndex.normalize("Acme Limited") == "acme"

    def test_normalize_preserves_meaningful_text(self) -> None:
        assert AliasIndex.normalize("Google") == "google"

    def test_normalize_collapses_whitespace(self) -> None:
        assert AliasIndex.normalize("  Apple   Inc.  ") == "apple"

    def test_empty_string(self) -> None:
        assert AliasIndex.normalize("") == ""

    def test_lookup_returns_empty_for_missing(self) -> None:
        idx = AliasIndex()
        assert idx.lookup("nonexistent") == []

    def test_len(self) -> None:
        idx = AliasIndex()
        idx.add("Apple", IndexEntry("1", "AAPL", "Apple Inc.", "alias"))
        idx.add("Google", IndexEntry("2", "GOOGL", "Alphabet Inc.", "alias"))
        assert len(idx) == 2

    def test_keys_returns_all_normalized_keys(self) -> None:
        idx = AliasIndex()
        idx.add("Apple", IndexEntry("1", "AAPL", "Apple Inc.", "alias"))
        idx.add("Google", IndexEntry("2", "GOOGL", "Alphabet Inc.", "alias"))
        keys = idx.keys()
        assert "apple" in keys
        assert "google" in keys


# --- Tests: Explicit vs Inferred Classification (Task 24.3) ---


class TestClassifyMention:
    """Test classify_mention separates explicit from inferred exposures."""

    def test_explicit_with_direct_mention(self) -> None:
        """Company name in text with no relationship keywords → explicit."""
        candidates = [
            ResolutionCandidate(
                company_id="11111111-1111-1111-1111-111111111111",
                ticker="AAPL",
                name="Apple Inc.",
                confidence=0.95,
                match_type=MatchType.exact_ticker,
            )
        ]
        context = "Apple announced record quarterly revenue of $94.8 billion."
        result = classify_mention("Apple", context, candidates)
        assert result == ClassifiedMentionType.explicit_mention

    def test_inferred_with_competitor_keyword(self) -> None:
        """Mention surrounded by competitor keywords → inferred."""
        candidates = [
            ResolutionCandidate(
                company_id="33333333-3333-3333-3333-333333333333",
                ticker="MSFT",
                name="Microsoft Corporation",
                confidence=0.80,
                match_type=MatchType.alias,
            )
        ]
        context = "Apple's main competitor Microsoft may feel pressure from the announcement."
        result = classify_mention("Microsoft", context, candidates)
        assert result == ClassifiedMentionType.inferred_exposure

    def test_inferred_with_supplier_keyword(self) -> None:
        """Mention with supplier relationship keyword → inferred."""
        candidates = [
            ResolutionCandidate(
                company_id="22222222-2222-2222-2222-222222222222",
                ticker="NVDA",
                name="NVIDIA Corporation",
                confidence=0.95,
                match_type=MatchType.exact_ticker,
            )
        ]
        context = "Tesla's key supplier NVDA could benefit from increased production volumes."
        result = classify_mention("NVDA", context, candidates)
        assert result == ClassifiedMentionType.inferred_exposure

    def test_unresolved_with_no_candidates(self) -> None:
        """No candidates → unresolved."""
        context = "Palantir Technologies posted strong growth numbers."
        result = classify_mention("Palantir", context, [])
        assert result == ClassifiedMentionType.unresolved

    def test_explicit_even_with_relationship_word_when_attributed(self) -> None:
        """If explicit attribution keywords are present near the mention, stay explicit."""
        candidates = [
            ResolutionCandidate(
                company_id="33333333-3333-3333-3333-333333333333",
                ticker="MSFT",
                name="Microsoft Corporation",
                confidence=0.90,
                match_type=MatchType.exact_name,
            )
        ]
        # "Microsoft announced" is explicit attribution even with "competitor" nearby
        context = "Microsoft announced earnings that beat competitor expectations."
        result = classify_mention("Microsoft", context, candidates)
        assert result == ClassifiedMentionType.explicit_mention

    def test_inferred_with_peer_keyword(self) -> None:
        """Sector peer reference → inferred."""
        candidates = [
            ResolutionCandidate(
                company_id="44444444-4444-4444-4444-444444444444",
                ticker="AMD",
                name="Advanced Micro Devices Inc.",
                confidence=0.80,
                match_type=MatchType.alias,
            )
        ]
        context = "NVIDIA's results could impact sector peer AMD through changed market expectations."
        result = classify_mention("AMD", context, candidates)
        assert result == ClassifiedMentionType.inferred_exposure

    def test_explicit_when_mention_not_in_context(self) -> None:
        """If mention not found in context at all, default to explicit (alias match suffices)."""
        candidates = [
            ResolutionCandidate(
                company_id="11111111-1111-1111-1111-111111111111",
                ticker="AAPL",
                name="Apple Inc.",
                confidence=0.95,
                match_type=MatchType.exact_ticker,
            )
        ]
        # Context doesn't contain the mention text
        context = "Revenue increased significantly in Q4."
        result = classify_mention("AAPL", context, candidates)
        assert result == ClassifiedMentionType.explicit_mention

    def test_explicit_with_empty_context(self) -> None:
        """Empty context with valid candidates → explicit (alias resolution is enough)."""
        candidates = [
            ResolutionCandidate(
                company_id="11111111-1111-1111-1111-111111111111",
                ticker="AAPL",
                name="Apple Inc.",
                confidence=0.95,
                match_type=MatchType.exact_ticker,
            )
        ]
        result = classify_mention("AAPL", "", candidates)
        assert result == ClassifiedMentionType.explicit_mention

    def test_to_mention_type_explicit(self) -> None:
        """ClassifiedMentionType.explicit_mention → MentionType.explicit."""
        assert to_mention_type(ClassifiedMentionType.explicit_mention) == MentionType.explicit

    def test_to_mention_type_inferred(self) -> None:
        """ClassifiedMentionType.inferred_exposure → MentionType.inferred."""
        assert to_mention_type(ClassifiedMentionType.inferred_exposure) == MentionType.inferred

    def test_to_mention_type_unresolved(self) -> None:
        """ClassifiedMentionType.unresolved → MentionType.explicit (default)."""
        assert to_mention_type(ClassifiedMentionType.unresolved) == MentionType.explicit

    def test_inferred_with_exposure_keyword(self) -> None:
        """Direct use of 'exposure' keyword → inferred."""
        candidates = [
            ResolutionCandidate(
                company_id="55555555-5555-5555-5555-555555555555",
                ticker="INTC",
                name="Intel Corporation",
                confidence=0.80,
                match_type=MatchType.alias,
            )
        ]
        context = "The tariffs create indirect exposure for INTC through its Asia supply chain."
        result = classify_mention("INTC", context, candidates)
        assert result == ClassifiedMentionType.inferred_exposure

    def test_enum_values(self) -> None:
        """Verify ClassifiedMentionType string values."""
        assert ClassifiedMentionType.explicit_mention.value == "explicit_mention"
        assert ClassifiedMentionType.inferred_exposure.value == "inferred_exposure"
        assert ClassifiedMentionType.unresolved.value == "unresolved"
