from __future__ import annotations

from datetime import datetime
from types import SimpleNamespace
from typing import Any

import numpy as np

from cognitive_signal.models.types import JournalEntry
from cognitive_signal.signals.extractors import (
    ActionRatioExtractor,
    AgencyExtractor,
    ArousalExtractor,
    SemanticEmbedder,
    SelfFocusExtractor,
    SignalExtractor,
    SocialOrientationExtractor,
    ValenceExtractor,
)


class FakeModel:
    def __init__(self, mapping: dict[str, np.ndarray]):
        self.mapping = mapping

    def encode(self, text: str, convert_to_numpy: bool = True) -> np.ndarray:
        return self.mapping[text]


class FakeAnalyzer:
    def __init__(self, compounds: dict[str, float]):
        self.compounds = compounds

    def polarity_scores(self, text: str) -> dict[str, float]:
        return {"compound": self.compounds[text]}


class FakeToken:
    def __init__(
        self,
        text: str,
        *,
        dep_: str = "",
        pos_: str = "",
        lemma_: str = "",
        head_pos: str = "",
    ):
        self.text = text
        self.dep_ = dep_
        self.pos_ = pos_
        self.lemma_ = lemma_ or text.lower()
        self.head = SimpleNamespace(pos_=head_pos)
        self.is_space = False


class FakeEntity:
    def __init__(self, label_: str):
        self.label_ = label_


class FakeDoc(list):
    def __init__(self, tokens: list[FakeToken], ents: list[FakeEntity] | None = None):
        super().__init__(tokens)
        self.ents = ents or []


class FakeNLP:
    def __init__(self, docs_by_text: dict[str, FakeDoc]):
        self.docs_by_text = docs_by_text

    def __call__(self, text: str) -> FakeDoc:
        return self.docs_by_text[text]


def _make_doc(
    token_specs: list[dict[str, Any]], ents: list[str] | None = None
) -> FakeDoc:
    tokens = [FakeToken(**spec) for spec in token_specs]
    entities = [FakeEntity(label) for label in (ents or [])]
    return FakeDoc(tokens, ents=entities)


def test_semantic_embedder_with_three_examples() -> None:
    vec_ones = np.ones(384, dtype=np.float32)
    vec_twos = np.full(384, 2.0, dtype=np.float32)
    vec_range = np.arange(384, dtype=np.float32)
    embedder = SemanticEmbedder(
        model=FakeModel(
            {
                "first example": vec_ones,
                "second example": vec_twos,
                "third example": vec_range,
            }
        )
    )

    for text in ["first example", "second example", "third example"]:
        embedding = embedder.embed(text)
        assert isinstance(embedding, np.ndarray)
        assert embedding.shape == (384,)


def test_valence_extractor_with_three_examples() -> None:
    extractor = ValenceExtractor(
        analyzer=FakeAnalyzer(
            {
                "I feel great today.": 0.8,
                "This is a terrible day.": -0.7,
                "It is an average afternoon.": 0.0,
            }
        )
    )

    assert extractor.extract("I feel great today.") > 0.3
    assert extractor.extract("This is a terrible day.") < -0.3
    assert -0.1 <= extractor.extract("It is an average afternoon.") <= 0.1


def test_arousal_extractor_with_three_examples_and_required_sentences() -> None:
    extractor = ArousalExtractor()

    assert extractor.extract("Everything is AMAZING and WONDERFUL!!!") > 0.7
    assert extractor.extract("It was okay I guess") < 0.3
    medium = extractor.extract("I am excited for tomorrow and a little nervous!")
    assert 0.2 <= medium <= 0.8


def test_arousal_emotional_entry_nonzero() -> None:
    extractor = ArousalExtractor()
    assert extractor.extract("I feel good that I finished the project") > 0.1


def test_arousal_very_emotional_entry_higher() -> None:
    extractor = ArousalExtractor()
    calm = extractor.extract("It was an okay day")
    excited = extractor.extract("I am absolutely thrilled and ecstatic about this")
    assert excited > calm
    assert excited > 0.5


def test_arousal_neutral_entry_low() -> None:
    extractor = ArousalExtractor()
    score = extractor.extract("Hello mic testing mic testing")
    assert score < 0.15


def test_arousal_intensity_modifiers() -> None:
    extractor = ArousalExtractor()
    base = extractor.extract("I am happy")
    boosted = extractor.extract("I am extremely happy")
    assert boosted > base


def test_arousal_finally_keyword() -> None:
    extractor = ArousalExtractor()
    score = extractor.extract("Finally getting my spark back")
    assert score > 0.2


def test_agency_extractor_with_three_examples_and_required_sentences() -> None:
    text_active = "I decided to quit my job"
    text_passive = "I was let go by management"
    text_neutral = "There is rain outside"

    docs = {
        text_active: _make_doc(
            [
                {"text": "I", "dep_": "nsubj", "head_pos": "VERB"},
                {"text": "decided", "pos_": "VERB", "lemma_": "decide"},
            ]
        ),
        text_passive: _make_doc(
            [
                {"text": "I", "dep_": "nsubjpass", "head_pos": "VERB"},
                {"text": "was", "dep_": "auxpass", "pos_": "AUX", "lemma_": "be"},
                {"text": "let", "pos_": "VERB", "lemma_": "let"},
            ]
        ),
        text_neutral: _make_doc(
            [
                {"text": "There", "pos_": "PRON", "lemma_": "there"},
                {"text": "rain", "pos_": "NOUN", "lemma_": "rain"},
            ]
        ),
    }
    extractor = AgencyExtractor(nlp=FakeNLP(docs))

    assert extractor.extract(text_active) > 0.5
    assert extractor.extract(text_passive) < 0.0
    assert extractor.extract(text_neutral) == 0.0


def test_self_focus_extractor_with_three_examples() -> None:
    extractor = SelfFocusExtractor()

    assert extractor.extract("I told myself that my choices matter.") > 0.8
    assert extractor.extract("They said she would call him later.") < 0.2
    assert extractor.extract("Rain on windows and quiet evenings.") == 0.5


def test_social_orientation_extractor_with_three_examples() -> None:
    text_high = "Sarah said she would call them later"
    text_low = "I walked quietly through the park"
    text_mid = "They asked for an update"

    docs = {
        text_high: _make_doc(
            [
                {"text": "Sarah", "pos_": "PROPN"},
                {"text": "said", "pos_": "VERB", "lemma_": "say"},
                {"text": "she", "pos_": "PRON"},
                {"text": "would", "pos_": "AUX", "lemma_": "would"},
                {"text": "call", "pos_": "VERB", "lemma_": "call"},
                {"text": "them", "pos_": "PRON"},
                {"text": "later", "pos_": "ADV"},
            ],
            ents=["PERSON"],
        ),
        text_low: _make_doc(
            [
                {"text": "I", "pos_": "PRON"},
                {"text": "walked", "pos_": "VERB", "lemma_": "walk"},
                {"text": "quietly", "pos_": "ADV"},
                {"text": "through", "pos_": "ADP"},
                {"text": "the", "pos_": "DET"},
                {"text": "park", "pos_": "NOUN"},
            ]
        ),
        text_mid: _make_doc(
            [
                {"text": "They", "pos_": "PRON"},
                {"text": "asked", "pos_": "VERB", "lemma_": "ask"},
                {"text": "for", "pos_": "ADP"},
                {"text": "an", "pos_": "DET"},
                {"text": "update", "pos_": "NOUN"},
            ]
        ),
    }
    extractor = SocialOrientationExtractor(nlp=FakeNLP(docs))

    high = extractor.extract(text_high)
    low = extractor.extract(text_low)
    mid = extractor.extract(text_mid)
    assert high > mid > low
    assert 0.0 <= low <= 1.0
    assert 0.0 <= mid <= 1.0
    assert 0.0 <= high <= 1.0


def test_action_ratio_extractor_with_three_examples() -> None:
    text_action = "I run build and send updates"
    text_cognitive = "I think and wonder and feel"
    text_balanced = "I write and think"

    docs = {
        text_action: _make_doc(
            [
                {"text": "run", "pos_": "VERB", "lemma_": "run"},
                {"text": "build", "pos_": "VERB", "lemma_": "build"},
                {"text": "send", "pos_": "VERB", "lemma_": "send"},
            ]
        ),
        text_cognitive: _make_doc(
            [
                {"text": "think", "pos_": "VERB", "lemma_": "think"},
                {"text": "wonder", "pos_": "VERB", "lemma_": "wonder"},
                {"text": "feel", "pos_": "VERB", "lemma_": "feel"},
            ]
        ),
        text_balanced: _make_doc(
            [
                {"text": "write", "pos_": "VERB", "lemma_": "write"},
                {"text": "think", "pos_": "VERB", "lemma_": "think"},
            ]
        ),
    }
    extractor = ActionRatioExtractor(nlp=FakeNLP(docs))

    assert extractor.extract(text_action) > 0.7
    assert extractor.extract(text_cognitive) < 0.3
    assert extractor.extract(text_balanced) == 0.5


def test_signal_extractor_returns_signal_vector_with_valid_ranges() -> None:
    class StubEmbedding:
        def embed(self, text: str) -> np.ndarray:
            _ = text
            return np.ones(384, dtype=np.float32)

    class StubScalar:
        def __init__(self, value: float):
            self.value = value

        def extract(self, text: str) -> float:
            _ = text
            return self.value

    extractor = SignalExtractor(
        semantic_embedder=StubEmbedding(),
        valence_extractor=StubScalar(0.2),
        arousal_extractor=StubScalar(0.8),
        agency_extractor=StubScalar(0.6),
        self_focus_extractor=StubScalar(0.7),
        social_orientation_extractor=StubScalar(0.4),
        action_ratio_extractor=StubScalar(0.5),
    )

    entry = JournalEntry(
        text="I decided to call Sarah after work.",
        timestamp=datetime(2025, 1, 1, 9, 0, 0),
        entry_id="entry_001",
    )
    vector = extractor.extract(entry)

    assert vector.embedding.shape == (384,)
    assert -1.0 <= vector.valence <= 1.0
    assert 0.0 <= vector.arousal <= 1.0
    assert -1.0 <= vector.agency <= 1.0
    assert 0.0 <= vector.self_focus <= 1.0
    assert 0.0 <= vector.social_orientation <= 1.0
    assert 0.0 <= vector.action_ratio <= 1.0
