"""
BIBIVIDI — simulate command tests

Note: Click's CliRunner is synchronous; tests must be plain `def`
(not async) to avoid the "asyncio.run() from a running event loop" error
that occurs when pytest-asyncio is already managing an event loop.

Rich's Console is bound to the real sys.stdout at module-load time, so
CliRunner's captured output never sees Rich output.  We patch `bvd.cli.console`
with a StringIO-backed Console and inspect its buffer directly.

library.init() opens an aiosqlite connection that outlives asyncio.run(); we
patch init/close to avoid the open connection that would cause pytest cleanup
to hang.
"""
from __future__ import annotations

import json
from contextlib import ExitStack
from io import StringIO
from unittest.mock import AsyncMock, patch

from click.testing import CliRunner
from rich.console import Console as RichConsole

from bvd.cli import main
from bvd.engine.library import Script
from bvd.engine.matcher import MatchResult
from bvd.engine.runner import RunResult

_SCRIPT = Script(
    id="sim-skill",
    name="Sim Skill",
    description="Used in simulate tests",
    code='print("[SIM]")',
    status="active",
)
_MATCH = MatchResult(script=_SCRIPT, script_id=_SCRIPT.id, score=0.90)
_RUN = RunResult(
    output="[SIM CONDENSED]",
    stdout="[SIM CONDENSED]",
    stderr="",
    exit_code=0,
    timed_out=False,
)


def _write_requests(tmp_path, requests: list) -> str:
    p = tmp_path / "requests.json"
    p.write_text(json.dumps(requests))
    return str(p)


def _make_console() -> tuple[RichConsole, StringIO]:
    """Return a (Console, buffer) pair for inspecting Rich output in tests."""
    buf = StringIO()
    con = RichConsole(file=buf, highlight=False, no_color=True, width=120)
    return con, buf


def test_simulate_matched_request(tmp_path):
    """simulate reports a match and savings for a matching request."""
    reqs = [{"messages": [{"role": "user", "content": "Summarise this contract."}]}]
    req_file = _write_requests(tmp_path, reqs)

    con, buf = _make_console()

    with ExitStack() as stack:
        stack.enter_context(patch("bvd.cli.console", con))
        stack.enter_context(patch("bvd.engine.library.library.init", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.close", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.all_with_embeddings",
                                  AsyncMock(return_value=[_SCRIPT])))
        stack.enter_context(patch("bvd.engine.matcher.Matcher.match",
                                  AsyncMock(return_value=_MATCH)))
        stack.enter_context(patch("bvd.engine.runner.run_script",
                                  AsyncMock(return_value=_RUN)))

        runner = CliRunner()
        result = runner.invoke(main, ["simulate", req_file])

    assert result.exit_code == 0, result.output
    output = buf.getvalue()
    assert "sim-skill" in output
    assert "Summary" in output


def test_simulate_unmatched_request(tmp_path):
    """simulate shows no match for a request that doesn't hit threshold."""
    reqs = [{"messages": [{"role": "user", "content": "What is the weather today?"}]}]
    req_file = _write_requests(tmp_path, reqs)

    con, buf = _make_console()

    with ExitStack() as stack:
        stack.enter_context(patch("bvd.cli.console", con))
        stack.enter_context(patch("bvd.engine.library.library.init", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.close", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.all_with_embeddings",
                                  AsyncMock(return_value=[_SCRIPT])))
        stack.enter_context(patch("bvd.engine.matcher.Matcher.match",
                                  AsyncMock(return_value=None)))

        runner = CliRunner()
        result = runner.invoke(main, ["simulate", req_file])

    assert result.exit_code == 0, result.output
    output = buf.getvalue()
    assert "Summary" in output


def test_simulate_invalid_json(tmp_path):
    """simulate exits non-zero on malformed JSON."""
    bad = tmp_path / "bad.json"
    bad.write_text("not json at all")

    con, buf = _make_console()

    with ExitStack() as stack:
        stack.enter_context(patch("bvd.cli.console", con))
        stack.enter_context(patch("bvd.engine.library.library.init", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.close", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.all_with_embeddings",
                                  AsyncMock(return_value=[_SCRIPT])))

        runner = CliRunner()
        result = runner.invoke(main, ["simulate", str(bad)])

    output = buf.getvalue()
    assert result.exit_code != 0 or "Invalid JSON" in output


def test_simulate_not_a_list(tmp_path):
    """simulate exits non-zero if JSON is not an array."""
    obj = tmp_path / "obj.json"
    obj.write_text(json.dumps({"messages": []}))

    con, buf = _make_console()

    with ExitStack() as stack:
        stack.enter_context(patch("bvd.cli.console", con))
        stack.enter_context(patch("bvd.engine.library.library.init", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.close", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.all_with_embeddings",
                                  AsyncMock(return_value=[_SCRIPT])))

        runner = CliRunner()
        result = runner.invoke(main, ["simulate", str(obj)])

    output = buf.getvalue()
    assert result.exit_code != 0 or "array" in output


def test_simulate_no_scripts(tmp_path):
    """simulate exits early when no embedded skills are installed."""
    reqs = [{"messages": [{"role": "user", "content": "Hello"}]}]
    req_file = _write_requests(tmp_path, reqs)

    con, buf = _make_console()

    with ExitStack() as stack:
        stack.enter_context(patch("bvd.cli.console", con))
        stack.enter_context(patch("bvd.engine.library.library.init", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.close", AsyncMock()))
        stack.enter_context(patch("bvd.engine.library.library.all_with_embeddings",
                                  AsyncMock(return_value=[])))

        runner = CliRunner()
        result = runner.invoke(main, ["simulate", req_file])

    assert result.exit_code == 0, result.output
    output = buf.getvalue()
    assert "No scripts" in output or "skills" in output.lower()
