"""Tests for the cortex dashboard web UI."""
import time
from unittest.mock import AsyncMock, MagicMock

import httpx
import pytest
from starlette.testclient import TestClient

from neo_cortex.dashboard import create_app, start_dashboard


# --- Fixtures ---

@pytest.fixture
def mock_cortex():
    cortex = MagicMock()
    cortex.stats.return_value = MagicMock(
        total=100, sessions=5, avg_energy=0.85, dream_count=3,
        model_dump=lambda: {"total": 100, "sessions": 5, "avg_energy": 0.85, "dream_count": 3},
    )
    cortex._index = MagicMock()
    cortex._index.count.return_value = 100
    cortex._index.stats.return_value = {
        "total": 100, "sessions": 5, "avg_energy": 0.85,
        "dream_count": 3, "projects": {"neo-cortex": 60, "general": 40},
    }
    cortex._graph = MagicMock()
    cortex._graph.node_count.return_value = 50
    cortex._graph._graph = MagicMock()
    cortex.dream_count = 3
    return cortex


@pytest.fixture
def app(mock_cortex):
    return create_app(mock_cortex)


@pytest.fixture
def client(app):
    return TestClient(app)


# --- TDDAB-1: Server startup + discovery ---

class TestDashboardStartup:
    """Dashboard starts on dynamic port and is discoverable."""

    def test_create_app_returns_fastapi(self, app):
        from fastapi import FastAPI
        assert isinstance(app, FastAPI)

    def test_create_app_has_routes(self, app):
        paths = [r.path for r in app.routes]
        assert "/" in paths
        assert "/api/stats" in paths

    def test_start_dashboard_returns_port(self, mock_cortex):
        port = start_dashboard(mock_cortex)
        assert isinstance(port, int)
        assert port > 0
        # Verify server is reachable
        time.sleep(0.5)
        resp = httpx.get(f"http://127.0.0.1:{port}/api/stats", timeout=5)
        assert resp.status_code == 200

    def test_start_dashboard_dynamic_port(self, mock_cortex):
        """Two instances get different ports."""
        port1 = start_dashboard(mock_cortex)
        port2 = start_dashboard(mock_cortex)
        assert port1 != port2
        assert port1 > 0
        assert port2 > 0


# --- TDDAB-2: Search + Timeline + Memory Detail API ---

class TestMemoryAPI:
    """Search, timeline, and memory detail endpoints."""

    def test_search_fts(self, client, mock_cortex):
        mock_cortex._index.search_fts.return_value = [
            MagicMock(model_dump=lambda: {"id": "m1", "title": "Test", "project": "p1"}),
        ]
        resp = client.get("/api/memories?q=test&n=10")
        assert resp.status_code == 200
        data = resp.json()
        assert "results" in data
        assert len(data["results"]) == 1
        mock_cortex._index.search_fts.assert_called_once()

    def test_search_with_project_filter(self, client, mock_cortex):
        mock_cortex._index.search_fts.return_value = []
        resp = client.get("/api/memories?q=test&project=neo-cortex")
        assert resp.status_code == 200
        mock_cortex._index.search_fts.assert_called_once()
        call_kwargs = mock_cortex._index.search_fts.call_args
        assert call_kwargs.kwargs.get("project") == "neo-cortex" or call_kwargs[1].get("project") == "neo-cortex"

    def test_timeline(self, client, mock_cortex):
        mock_cortex._index.timeline.return_value = [
            MagicMock(model_dump=lambda: {"id": "m1", "title": "Recent", "timestamp": 1000}),
        ]
        resp = client.get("/api/timeline?n=5")
        assert resp.status_code == 200
        data = resp.json()
        assert "memories" in data
        assert len(data["memories"]) == 1

    def test_memory_detail(self, client, mock_cortex):
        mock_cortex._index.get_by_id.return_value = MagicMock(
            model_dump=lambda: {"id": "m1", "question": "test?", "answer": "yes"}
        )
        mock_cortex._index.get_structured.return_value = MagicMock(
            model_dump=lambda: {"title": "Test", "summary": "A test", "concepts": ["testing"]}
        )
        resp = client.get("/api/memory/m1")
        assert resp.status_code == 200
        data = resp.json()
        assert data["record"]["id"] == "m1"
        assert data["structured"]["title"] == "Test"

    def test_memory_detail_not_found(self, client, mock_cortex):
        mock_cortex._index.get_by_id.return_value = None
        resp = client.get("/api/memory/nonexistent")
        assert resp.status_code == 404


# --- TDDAB-3: Concept Graph API + Energy Distribution ---

class TestGraphAPI:
    """Concept graph and energy distribution endpoints."""

    def test_graph_returns_nodes_and_edges(self, client, mock_cortex):
        import networkx as nx
        G = nx.Graph()
        G.add_node("python", mentions=10)
        G.add_node("testing", mentions=5)
        G.add_edge("python", "testing", weight=3)
        mock_cortex._graph._graph = G
        mock_cortex._graph._memory_concepts = {"m1": ["python", "testing"]}

        resp = client.get("/api/graph")
        assert resp.status_code == 200
        data = resp.json()
        assert "nodes" in data
        assert "edges" in data
        assert len(data["nodes"]) == 2
        assert len(data["edges"]) == 1
        # Nodes have name + mentions
        node_names = {n["name"] for n in data["nodes"]}
        assert "python" in node_names
        # Edges have source + target + weight
        edge = data["edges"][0]
        assert "source" in edge
        assert "target" in edge
        assert "weight" in edge

    def test_graph_concept_detail(self, client, mock_cortex):
        mock_cortex._graph.memories_for.return_value = ["m1", "m2"]
        mock_cortex._graph.spreading_activation.return_value = {"python": 1.0, "testing": 0.5}
        mock_cortex._index.get_by_ids.return_value = [
            MagicMock(model_dump=lambda: {"id": "m1", "topic": "Topic 1"}),
            MagicMock(model_dump=lambda: {"id": "m2", "topic": "Topic 2"}),
        ]
        resp = client.get("/api/graph/concept/python")
        assert resp.status_code == 200
        data = resp.json()
        assert data["concept"] == "python"
        assert "memories" in data
        assert "related" in data

    def test_energy_distribution(self, client, mock_cortex):
        mock_cortex._index.get_all_for_dream.return_value = [
            {"id": "m1", "energy": 0.95, "stability": 0.8, "project": "neo-cortex"},
            {"id": "m2", "energy": 0.50, "stability": 0.3, "project": "general"},
            {"id": "m3", "energy": 0.10, "stability": 0.1, "project": "neo-cortex"},
        ]
        resp = client.get("/api/energy")
        assert resp.status_code == 200
        data = resp.json()
        assert "histogram" in data
        assert "by_project" in data
        # Histogram has 10 buckets (0.0-0.1, 0.1-0.2, ..., 0.9-1.0)
        assert len(data["histogram"]) == 10

    def test_graph_empty(self, client, mock_cortex):
        import networkx as nx
        mock_cortex._graph._graph = nx.Graph()
        mock_cortex._graph._memory_concepts = {}
        resp = client.get("/api/graph")
        assert resp.status_code == 200
        data = resp.json()
        assert data["nodes"] == []
        assert data["edges"] == []


# --- TDDAB-4: Action Endpoints (Dream + Rebuild) ---

class TestActions:
    """Action endpoints: dream, rebuild."""

    def test_dream(self, client, mock_cortex):
        mock_cortex.dream = AsyncMock(return_value=MagicMock(
            status="completed", dream_count=4,
            cluster=["m1", "m2"],
            model_dump=lambda: {"status": "completed", "dream_count": 4},
        ))
        resp = client.post("/api/dream")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "completed"
        mock_cortex.dream.assert_called_once()

    def test_dream_error(self, client, mock_cortex):
        mock_cortex.dream = AsyncMock(side_effect=RuntimeError("dream failed"))
        resp = client.post("/api/dream")
        assert resp.status_code == 500
        assert "error" in resp.json()

    def test_rebuild_requires_confirmation(self, client, mock_cortex):
        """Rebuild is destructive — require confirm=true."""
        resp = client.post("/api/rebuild")
        assert resp.status_code == 400
        assert "confirm" in resp.json().get("error", "").lower()

    def test_rebuild_with_confirmation(self, client, mock_cortex):
        mock_cortex._store = MagicMock()
        mock_cortex._store.clear.return_value = 100
        mock_cortex._index.clear.return_value = 100
        mock_cortex._dedup = MagicMock()
        mock_cortex._dedup.clear.return_value = 50
        mock_cortex._graph.clear.return_value = 30
        mock_cortex._index.clear_pending_merges.return_value = 5
        mock_cortex._dream_count = 3
        resp = client.post("/api/rebuild?confirm=true")
        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "rebuilt"


# --- TDDAB-5: HTML Frontend ---

class TestFrontend:
    """HTML frontend tests."""

    def test_index_returns_html(self, client):
        resp = client.get("/")
        assert resp.status_code == 200
        assert "text/html" in resp.headers["content-type"]

    def test_index_contains_chart_js(self, client):
        resp = client.get("/")
        assert "chart.js" in resp.text.lower() or "Chart" in resp.text

    def test_index_contains_d3(self, client):
        resp = client.get("/")
        assert "d3" in resp.text.lower()

    def test_index_contains_search(self, client):
        resp = client.get("/")
        assert "search" in resp.text.lower()

    def test_index_contains_nav_sections(self, client):
        """Page has all main sections."""
        html = client.get("/").text
        for section in ["stats", "memories", "graph", "energy"]:
            assert section in html.lower(), f"Missing section: {section}"
