"""Tests for sequence reconstruction module."""

from __future__ import annotations

from datetime import datetime, timedelta
from typing import List
from unittest.mock import MagicMock

import networkx as nx
import pytest

from hieromem_core.reasoning.sequence import (
    SequenceType,
    SequenceNode,
    SequenceGap,
    ReconstructedSequence,
    SequenceReconstructor,
)


class TestSequenceType:
    """Tests for SequenceType enum."""

    def test_temporal_value(self) -> None:
        assert SequenceType.TEMPORAL.value == "temporal"

    def test_causal_value(self) -> None:
        assert SequenceType.CAUSAL.value == "causal"

    def test_narrative_value(self) -> None:
        assert SequenceType.NARRATIVE.value == "narrative"

    def test_procedural_value(self) -> None:
        assert SequenceType.PROCEDURAL.value == "procedural"


class TestSequenceNode:
    """Tests for SequenceNode dataclass."""

    def test_create_basic_node(self) -> None:
        node = SequenceNode(episode_id="ep-1", position=0)

        assert node.episode_id == "ep-1"
        assert node.position == 0
        assert node.timestamp is None
        assert node.trust == 1.0
        assert node.role is None

    def test_create_node_with_all_fields(self) -> None:
        ts = datetime(2024, 1, 1, 12, 0)
        node = SequenceNode(
            episode_id="ep-2",
            position=5,
            timestamp=ts,
            content_preview="Hello world",
            trust=0.8,
            role="milestone",
            metadata={"key": "value"},
        )

        assert node.timestamp == ts
        assert node.content_preview == "Hello world"
        assert node.trust == 0.8
        assert node.role == "milestone"
        assert node.metadata == {"key": "value"}

    def test_to_dict(self) -> None:
        ts = datetime(2024, 1, 1, 12, 0)
        node = SequenceNode(
            episode_id="ep-1",
            position=0,
            timestamp=ts,
            content_preview="test",
            trust=0.9,
            role="start",
        )

        d = node.to_dict()

        assert d["episode_id"] == "ep-1"
        assert d["position"] == 0
        assert d["timestamp"] == ts.isoformat()
        assert d["content_preview"] == "test"
        assert d["trust"] == 0.9
        assert d["role"] == "start"

    def test_to_dict_without_timestamp(self) -> None:
        node = SequenceNode(episode_id="ep-1", position=0)
        d = node.to_dict()

        assert d["timestamp"] is None


class TestSequenceGap:
    """Tests for SequenceGap dataclass."""

    def test_create_gap(self) -> None:
        gap = SequenceGap(
            position=3,
            before_episode_id="ep-1",
            after_episode_id="ep-2",
        )

        assert gap.position == 3
        assert gap.before_episode_id == "ep-1"
        assert gap.after_episode_id == "ep-2"
        assert gap.time_gap is None
        assert gap.estimated_missing == 0
        assert gap.confidence == 0.5

    def test_create_gap_with_time(self) -> None:
        gap = SequenceGap(
            position=3,
            before_episode_id="ep-1",
            after_episode_id="ep-2",
            time_gap=timedelta(hours=8),
            estimated_missing=3,
            confidence=0.7,
        )

        assert gap.time_gap == timedelta(hours=8)
        assert gap.estimated_missing == 3
        assert gap.confidence == 0.7

    def test_to_dict(self) -> None:
        gap = SequenceGap(
            position=3,
            before_episode_id="ep-1",
            after_episode_id="ep-2",
            time_gap=timedelta(hours=2),
            estimated_missing=1,
            confidence=0.6,
        )

        d = gap.to_dict()

        assert d["position"] == 3
        assert d["before_episode_id"] == "ep-1"
        assert d["after_episode_id"] == "ep-2"
        assert d["time_gap_seconds"] == 7200.0  # 2 hours in seconds
        assert d["estimated_missing"] == 1
        assert d["confidence"] == 0.6

    def test_to_dict_without_time_gap(self) -> None:
        gap = SequenceGap(position=0, before_episode_id="a", after_episode_id="b")
        d = gap.to_dict()

        assert d["time_gap_seconds"] is None


class TestReconstructedSequence:
    """Tests for ReconstructedSequence dataclass."""

    @pytest.fixture
    def sample_nodes(self) -> List[SequenceNode]:
        return [
            SequenceNode(episode_id="ep-1", position=0, role="start"),
            SequenceNode(episode_id="ep-2", position=1),
            SequenceNode(episode_id="ep-3", position=2, role="end"),
        ]

    def test_create_sequence(self, sample_nodes: List[SequenceNode]) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=sample_nodes,
        )

        assert seq.sequence_type == SequenceType.TEMPORAL
        assert seq.nodes == sample_nodes
        assert seq.gaps == []
        assert seq.completeness == 1.0
        assert seq.confidence == 1.0

    def test_length_property(self, sample_nodes: List[SequenceNode]) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=sample_nodes,
        )

        assert seq.length == 3

    def test_start_node_property(self, sample_nodes: List[SequenceNode]) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=sample_nodes,
        )

        assert seq.start_node == sample_nodes[0]

    def test_end_node_property(self, sample_nodes: List[SequenceNode]) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=sample_nodes,
        )

        assert seq.end_node == sample_nodes[-1]

    def test_start_node_empty_sequence(self) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=[],
        )

        assert seq.start_node is None

    def test_end_node_empty_sequence(self) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=[],
        )

        assert seq.end_node is None

    def test_to_dict(self, sample_nodes: List[SequenceNode]) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.TEMPORAL,
            nodes=sample_nodes,
            total_duration=timedelta(hours=5),
            completeness=0.9,
            confidence=0.8,
            query="test query",
        )

        d = seq.to_dict()

        assert d["sequence_type"] == "temporal"
        assert len(d["nodes"]) == 3
        assert d["length"] == 3
        assert d["total_duration_seconds"] == 18000.0  # 5 hours
        assert d["completeness"] == 0.9
        assert d["confidence"] == 0.8
        assert d["query"] == "test query"

    def test_to_narrative(self) -> None:
        nodes = [
            SequenceNode(episode_id="ep-1", position=0, content_preview="Start"),
            SequenceNode(episode_id="ep-2", position=1, content_preview="Middle"),
            SequenceNode(episode_id="ep-3", position=2, content_preview="End"),
        ]
        seq = ReconstructedSequence(
            sequence_type=SequenceType.NARRATIVE,
            nodes=nodes,
        )

        narrative = seq.to_narrative()

        assert narrative == "Start → Middle → End"

    def test_to_narrative_custom_connector(self) -> None:
        nodes = [
            SequenceNode(episode_id="ep-1", position=0, content_preview="A"),
            SequenceNode(episode_id="ep-2", position=1, content_preview="B"),
        ]
        seq = ReconstructedSequence(
            sequence_type=SequenceType.NARRATIVE,
            nodes=nodes,
        )

        narrative = seq.to_narrative(connector=" -> ")

        assert narrative == "A -> B"

    def test_to_narrative_no_preview(self) -> None:
        nodes = [
            SequenceNode(episode_id="ep-1", position=0),
            SequenceNode(episode_id="ep-2", position=1),
        ]
        seq = ReconstructedSequence(
            sequence_type=SequenceType.NARRATIVE,
            nodes=nodes,
        )

        narrative = seq.to_narrative()

        assert "[ep-1]" in narrative
        assert "[ep-2]" in narrative

    def test_to_narrative_empty(self) -> None:
        seq = ReconstructedSequence(
            sequence_type=SequenceType.NARRATIVE,
            nodes=[],
        )

        assert seq.to_narrative() == ""


class TestSequenceReconstructor:
    """Tests for SequenceReconstructor."""

    @pytest.fixture
    def reconstructor(self) -> SequenceReconstructor:
        return SequenceReconstructor(
            gap_threshold=timedelta(hours=4),
            min_sequence_length=2,
            max_sequence_length=10,
        )

    @pytest.fixture
    def mock_episodes(self) -> List[MagicMock]:
        """Create mock episodes with timestamps."""
        episodes = []
        base_time = datetime(2024, 1, 1, 10, 0)

        for i in range(5):
            ep = MagicMock()
            ep.id = f"ep-{i}"
            ep.tocc = base_time + timedelta(hours=i)
            ep.trust = 0.9 - (i * 0.1)
            ep.meta = {"text": f"Event {i}"}
            ep.G = None
            episodes.append(ep)

        return episodes

    def test_init(self) -> None:
        r = SequenceReconstructor(
            gap_threshold=timedelta(hours=2),
            min_sequence_length=3,
            max_sequence_length=50,
        )

        assert r.gap_threshold == timedelta(hours=2)
        assert r.min_sequence_length == 3
        assert r.max_sequence_length == 50

    def test_reconstruct_temporal(
        self,
        reconstructor: SequenceReconstructor,
        mock_episodes: List[MagicMock],
    ) -> None:
        result = reconstructor.reconstruct_temporal(
            episodes=mock_episodes,
            query="test",
        )

        assert result.sequence_type == SequenceType.TEMPORAL
        assert result.length == 5
        assert result.nodes[0].episode_id == "ep-0"
        assert result.nodes[-1].episode_id == "ep-4"
        assert result.nodes[0].role == "start"
        assert result.nodes[-1].role == "end"
        assert result.query == "test"

    def test_reconstruct_temporal_with_time_filter(
        self,
        reconstructor: SequenceReconstructor,
        mock_episodes: List[MagicMock],
    ) -> None:
        # Filter to only include first 3 episodes
        start_time = datetime(2024, 1, 1, 10, 0)
        end_time = datetime(2024, 1, 1, 12, 30)

        result = reconstructor.reconstruct_temporal(
            episodes=mock_episodes,
            start_time=start_time,
            end_time=end_time,
        )

        assert result.length == 3

    def test_reconstruct_temporal_duration(
        self,
        reconstructor: SequenceReconstructor,
        mock_episodes: List[MagicMock],
    ) -> None:
        result = reconstructor.reconstruct_temporal(mock_episodes)

        # 5 episodes, 1 hour apart = 4 hours total duration
        assert result.total_duration == timedelta(hours=4)

    def test_reconstruct_temporal_empty(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        result = reconstructor.reconstruct_temporal(episodes=[])

        assert result.length == 0
        assert result.total_duration is None

    def test_reconstruct_temporal_detects_gaps(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        # Create episodes with a 6-hour gap (threshold is 4 hours)
        episodes = []
        base_time = datetime(2024, 1, 1, 10, 0)

        ep1 = MagicMock()
        ep1.id = "ep-1"
        ep1.tocc = base_time
        ep1.trust = 1.0
        ep1.meta = {}
        episodes.append(ep1)

        ep2 = MagicMock()
        ep2.id = "ep-2"
        ep2.tocc = base_time + timedelta(hours=6)  # Big gap
        ep2.trust = 1.0
        ep2.meta = {}
        episodes.append(ep2)

        result = reconstructor.reconstruct_temporal(episodes)

        assert len(result.gaps) == 1
        assert result.gaps[0].before_episode_id == "ep-1"
        assert result.gaps[0].after_episode_id == "ep-2"
        assert result.completeness < 1.0

    def test_reconstruct_causal(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        # Create episodes with causal edges
        start_ep = MagicMock()
        start_ep.id = "start"
        start_ep.trust = 1.0
        start_ep.tocc = None
        start_ep.meta = {}

        graph = nx.DiGraph()
        graph.add_edge("start", "middle", relation="causes")
        start_ep.G = graph

        middle_ep = MagicMock()
        middle_ep.id = "middle"
        middle_ep.trust = 0.9
        middle_ep.tocc = None
        middle_ep.meta = {}
        middle_ep.G = None

        episodes = [start_ep, middle_ep]

        result = reconstructor.reconstruct_causal(
            start_episode=start_ep,
            episodes=episodes,
        )

        assert result.sequence_type == SequenceType.CAUSAL
        assert result.nodes[0].episode_id == "start"
        assert len(result.gaps) == 0  # Causal chains don't have gaps

    def test_get_preview_with_text(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        ep = MagicMock()
        ep.meta = {"text": "Hello world"}

        preview = reconstructor._get_preview(ep)

        assert preview == "Hello world"

    def test_get_preview_truncates_long_text(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        ep = MagicMock()
        ep.meta = {"text": "A" * 150}

        preview = reconstructor._get_preview(ep)

        assert preview is not None
        assert len(preview) == 103  # 100 + "..."
        assert preview.endswith("...")

    def test_get_preview_no_meta(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        ep = MagicMock()
        ep.meta = None

        preview = reconstructor._get_preview(ep)

        assert preview is None

    def test_detect_temporal_gaps_no_gaps(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        nodes = [
            SequenceNode(
                episode_id=f"ep-{i}",
                position=i,
                timestamp=datetime(2024, 1, 1, 10 + i, 0),
            )
            for i in range(3)
        ]

        gaps = reconstructor._detect_temporal_gaps(nodes)

        assert len(gaps) == 0

    def test_estimate_completeness_no_gaps(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        nodes = [
            SequenceNode(episode_id=f"ep-{i}", position=i)
            for i in range(5)
        ]

        completeness = reconstructor._estimate_completeness(nodes, [])

        assert completeness == 1.0

    def test_estimate_completeness_with_gaps(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        nodes = [
            SequenceNode(episode_id=f"ep-{i}", position=i)
            for i in range(3)
        ]
        gaps = [
            SequenceGap(
                position=1,
                before_episode_id="ep-0",
                after_episode_id="ep-1",
                estimated_missing=2,
            ),
        ]

        completeness = reconstructor._estimate_completeness(nodes, gaps)

        # 3 nodes / (3 nodes + 2 missing) = 0.6
        assert completeness == pytest.approx(0.6)

    def test_estimate_completeness_empty(
        self,
        reconstructor: SequenceReconstructor,
    ) -> None:
        completeness = reconstructor._estimate_completeness([], [])

        assert completeness == 0.0
