"""
LLM Layer tests — get_why_ai, simulate_narrative, and CLI integration.

All httpx calls are mocked — no real network traffic.

Run: pytest tests/test_llm_layer.py -v
"""

from __future__ import annotations

import time
from unittest.mock import MagicMock, patch

import pytest

from kodara.memory.store import KodaraIndex
from kodara.intelligence.why import WhyResult, get_why_ai
from kodara.simulation.engine import SimulationResult, simulate_narrative


# ── Helpers ────────────────────────────────────────────────────────────────────


def _make_index(modules: dict | None = None) -> KodaraIndex:
    modules = modules or {
        "/fake/root/auth.py": {
            "module_name": "auth",
            "summary": "Handles authentication",
            "dependencies": [],
            "dependents": ["/fake/root/api.py"],
            "git": {
                "commit_count": 10,
                "last_message": "Add JWT support",
                "top_authors": [{"name": "alice", "commits": 8}],
            },
        }
    }
    return KodaraIndex(
        repo_id="local/test-00000000",
        display_name="test",
        source_root="/fake/root",
        indexed_at=time.time(),
        index_version=3,
        file_count=len(modules),
        language_counts={"python": len(modules)},
        modules=modules,
        graph_edges=[],
        file_hashes={p: "abc" for p in modules},
    )


def _make_sim_result(**kwargs) -> SimulationResult:
    defaults = dict(
        file="/fake/root/auth.py",
        module_name="auth",
        risk_score=15.0,
        risk_level="HIGH",
        direct_count=3,
        indirect_count=7,
        hub_hits=1,
        downstream_cost_hours=20.0,
        critical_paths=[["/fake/root/auth.py", "/fake/root/api.py"]],
        top_affected=["api", "admin", "tests"],
    )
    defaults.update(kwargs)
    return SimulationResult(**defaults)


def _mock_httpx_response(text: str, status_code: int = 200) -> MagicMock:
    """Build a minimal mock that looks like an httpx Response."""
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = {"text": text}
    return resp


_FAKE_SESSION = {"access_token": "test-token", "refresh_token": "rt", "email": "u@e.com", "expires_at": 9999999999}


# ── get_why_ai tests ───────────────────────────────────────────────────────────


def test_get_why_ai_returns_none_without_session():
    """get_why_ai returns None when no session exists."""
    idx = _make_index()
    with patch("kodara.cloud.client.load_session", return_value=None):
        result = get_why_ai(idx, "/fake/root/auth.py")
    assert result is None


def test_get_why_ai_returns_none_without_access_token():
    """get_why_ai returns None when session has no access_token."""
    idx = _make_index()
    empty_session = {"email": "u@e.com", "expires_at": 9999999999}
    with patch("kodara.cloud.client.load_session", return_value=empty_session):
        result = get_why_ai(idx, "/fake/root/auth.py")
    assert result is None


def test_get_why_ai_returns_string_when_session_exists():
    """get_why_ai returns an AI narrative string when session exists and backend returns 200."""
    idx = _make_index()
    mock_resp = _mock_httpx_response("This file handles user authentication.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp):
        result = get_why_ai(idx, "/fake/root/auth.py")

    assert isinstance(result, str)
    assert result == "This file handles user authentication."


def test_get_why_ai_returns_none_on_non_200():
    """get_why_ai returns None when backend responds with non-200 status."""
    idx = _make_index()
    mock_resp = _mock_httpx_response("", status_code=401)

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp):
        result = get_why_ai(idx, "/fake/root/auth.py")

    assert result is None


def test_get_why_ai_returns_none_on_exception():
    """get_why_ai returns None and does not crash when httpx raises."""
    idx = _make_index()

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", side_effect=RuntimeError("network error")):
        result = get_why_ai(idx, "/fake/root/auth.py")

    assert result is None


def test_get_why_ai_posts_to_correct_url():
    """get_why_ai posts to https://getkodara.dev/api/llm."""
    idx = _make_index()
    mock_resp = _mock_httpx_response("Narrative.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp) as mock_post:
        get_why_ai(idx, "/fake/root/auth.py")

    call_args = mock_post.call_args
    assert call_args.args[0] == "https://getkodara.dev/api/llm"


def test_get_why_ai_sends_session_token():
    """get_why_ai sends session_token in the request body."""
    idx = _make_index()
    mock_resp = _mock_httpx_response("Narrative.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp) as mock_post:
        get_why_ai(idx, "/fake/root/auth.py")

    body = mock_post.call_args.kwargs["json"]
    assert body["session_token"] == "test-token"


# ── simulate_narrative tests ───────────────────────────────────────────────────


def test_simulate_narrative_returns_none_without_session():
    """simulate_narrative returns None when no session exists."""
    sim = _make_sim_result()
    with patch("kodara.cloud.client.load_session", return_value=None):
        result = simulate_narrative(sim)
    assert result is None


def test_simulate_narrative_returns_string_when_session_exists():
    """simulate_narrative returns a narrative string when session exists and backend returns 200."""
    sim = _make_sim_result()
    mock_resp = _mock_httpx_response("This change carries high risk.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp):
        result = simulate_narrative(sim)

    assert isinstance(result, str)
    assert result == "This change carries high risk."


def test_simulate_narrative_returns_none_on_non_200():
    """simulate_narrative returns None when backend responds with non-200 status."""
    sim = _make_sim_result()
    mock_resp = _mock_httpx_response("", status_code=403)

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp):
        result = simulate_narrative(sim)

    assert result is None


def test_simulate_narrative_returns_none_on_exception():
    """simulate_narrative returns None and does not crash on httpx exception."""
    sim = _make_sim_result()

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", side_effect=ConnectionError("timeout")):
        result = simulate_narrative(sim)

    assert result is None


def test_simulate_narrative_posts_to_correct_url():
    """simulate_narrative posts to https://getkodara.dev/api/llm."""
    sim = _make_sim_result()
    mock_resp = _mock_httpx_response("Risk narrative.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp) as mock_post:
        simulate_narrative(sim)

    call_args = mock_post.call_args
    assert call_args.args[0] == "https://getkodara.dev/api/llm"


def test_simulate_narrative_sends_session_token():
    """simulate_narrative sends session_token in the request body."""
    sim = _make_sim_result()
    mock_resp = _mock_httpx_response("Risk narrative.")

    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp) as mock_post:
        simulate_narrative(sim)

    body = mock_post.call_args.kwargs["json"]
    assert body["session_token"] == "test-token"


# ── CLI integration tests ──────────────────────────────────────────────────────


def _build_cli_index(tmp_path):
    """Write a minimal .kodara index to tmp_path for CLI invocation."""
    import json
    from kodara.memory.store import MemoryStore

    root = tmp_path / "proj"
    root.mkdir()
    fake_file = root / "auth.py"
    fake_file.write_text("# auth module\n")

    store = MemoryStore(root)
    idx = KodaraIndex(
        repo_id="local/test-cli00",
        display_name="test",
        source_root=str(root),
        indexed_at=time.time(),
        index_version=3,
        file_count=1,
        language_counts={"python": 1},
        modules={
            str(fake_file): {
                "module_name": "auth",
                "summary": "Auth module",
                "dependencies": [],
                "dependents": [],
                "git": {
                    "commit_count": 2,
                    "last_message": "init auth",
                    "top_authors": [{"name": "bob", "commits": 2}],
                },
            }
        },
        graph_edges=[],
        file_hashes={str(fake_file): "abc"},
    )
    store.save(idx)
    return root, str(fake_file)


def test_cli_why_shows_ai_when_session_and_backend_200(tmp_path):
    """CLI 'why' shows 'AI:' when session exists and backend returns 200."""
    from click.testing import CliRunner
    from kodara.cli.main import why

    root, fake_file = _build_cli_index(tmp_path)
    mock_resp = _mock_httpx_response("This file handles auth.")

    runner = CliRunner()
    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("httpx.post", return_value=mock_resp):
        result = runner.invoke(why, ["--path", str(root), fake_file])

    assert result.exit_code == 0
    assert "AI:" in result.output
    assert "This file handles auth." in result.output


def test_cli_why_no_ai_without_session(tmp_path):
    """CLI 'why' does not show 'AI:' when no session exists."""
    from click.testing import CliRunner
    from kodara.cli.main import why

    root, fake_file = _build_cli_index(tmp_path)
    runner = CliRunner()

    with patch("kodara.cloud.client.load_session", return_value=None):
        result = runner.invoke(why, ["--path", str(root), fake_file])

    assert result.exit_code == 0
    assert "AI:" not in result.output


def test_cli_simulate_change_shows_ai_when_session_and_backend_200(tmp_path):
    """CLI 'simulate-change' shows 'AI:' when session exists and backend returns 200."""
    from click.testing import CliRunner
    from kodara.cli.main import simulate_change

    root, fake_file = _build_cli_index(tmp_path)
    mock_resp = _mock_httpx_response("High risk change detected.")
    mock_impact = {
        "target": fake_file, "target_module": "auth", "target_summary": "",
        "direct": [], "indirect": [], "risk_level": "LOW",
        "risk_reason": "Affects 0 modules", "critical_paths": [], "total_affected": 0,
    }

    runner = CliRunner()
    with patch("kodara.cloud.client.load_session", return_value=_FAKE_SESSION), \
         patch("kodara.engine_client._post", return_value=mock_impact), \
         patch("httpx.post", return_value=mock_resp):
        result = runner.invoke(simulate_change, ["--path", str(root), fake_file])

    assert result.exit_code == 0
    assert "AI:" in result.output
    assert "High risk change detected." in result.output


def test_cli_simulate_change_no_ai_without_session(tmp_path):
    """CLI 'simulate-change' does not show 'AI:' when no session exists."""
    from click.testing import CliRunner
    from kodara.cli.main import simulate_change

    root, fake_file = _build_cli_index(tmp_path)
    mock_impact = {
        "target": fake_file, "target_module": "auth", "target_summary": "",
        "direct": [], "indirect": [], "risk_level": "LOW",
        "risk_reason": "Affects 0 modules", "critical_paths": [], "total_affected": 0,
    }

    runner = CliRunner()
    with patch("kodara.cloud.client.load_session", return_value=None), \
         patch("kodara.engine_client._post", return_value=mock_impact):
        result = runner.invoke(simulate_change, ["--path", str(root), fake_file])

    assert result.exit_code == 0
    assert "AI:" not in result.output
