import json

import pytest

from analyst import Assistant, ReadOnlyDB
from fake_openai import fake_client

ANSWER = json.dumps({"summary": "ok", "key_numbers": [{"label": "orders", "value": "800"}],
                     "sql": ["SELECT COUNT(*) FROM orders"], "caveats": []})


@pytest.fixture(scope="module")
def db():
    return ReadOnlyDB()


def tool_outputs(client, request_index):
    return [i for i in client.fake.requests[request_index]["body"]["input"]
            if isinstance(i, dict) and i.get("type") == "function_call_output"]


def test_tool_loop_runs_sql_and_returns_structured_answer(db):
    client = fake_client(script=[{"tool_call": "run_sql", "arguments": {"query": "SELECT COUNT(*) FROM orders"}},
                                 ANSWER])
    answer, turn = Assistant(client, db).ask("How many orders?")
    assert answer.summary == "ok" and answer.key_numbers[0].value == "800"
    assert turn.tool_calls == [("run_sql", {"query": "SELECT COUNT(*) FROM orders"})]
    assert "800" in tool_outputs(client, 1)[0]["output"]              # the REAL result went back


def test_every_request_carries_instructions_schema_and_chaining(db):
    client = fake_client(script=[{"tool_call": "describe_tables", "arguments": {}}, ANSWER])
    Assistant(client, db).ask("What tables are there?")
    first, second = (r["body"] for r in client.fake.requests)
    assert "retail data analyst" in first["instructions"] and "retail data analyst" in second["instructions"]
    assert second["previous_response_id"].startswith("resp_")
    assert second["text"]["format"]["name"] == "Answer"
    assert "CREATE TABLE orders" in tool_outputs(client, 1)[0]["output"]


def test_rejected_sql_is_reported_to_the_model_and_the_loop_continues(db):
    client = fake_client(script=[
        {"tool_call": "run_sql", "arguments": {"query": "DELETE FROM orders"}},
        {"tool_call": "run_sql", "arguments": {"query": "SELECT COUNT(*) FROM orders"}},
        ANSWER,
    ])
    answer, turn = Assistant(client, db).ask("Delete everything")
    assert tool_outputs(client, 1)[0]["output"].startswith("error: query rejected")
    assert len(turn.tool_calls) == 2 and answer.summary == "ok"


def test_unknown_tool_is_an_error_output_not_a_crash(db):
    client = fake_client(script=[{"tool_call": "drop_database", "arguments": {}}, ANSWER])
    Assistant(client, db).ask("hi")
    assert "unknown tool" in tool_outputs(client, 1)[0]["output"]


def test_parallel_tool_calls_all_get_outputs(db):
    client = fake_client(script=[{"tool_calls": [("run_sql", {"query": "SELECT 1"}),
                                                 ("run_sql", {"query": "SELECT 2"})]}, ANSWER])
    Assistant(client, db).ask("two things")
    outputs = tool_outputs(client, 1)
    calls = [c for c in client.fake.requests]                                   # noqa: F841
    assert len(outputs) == 2 and outputs[0]["call_id"] != outputs[1]["call_id"]


def test_runaway_loops_are_capped(db):
    endless = [{"tool_call": "describe_tables", "arguments": {}}] * 10
    with pytest.raises(RuntimeError, match="tool rounds"):
        Assistant(fake_client(script=endless), db, max_rounds=3).ask("loop forever")


def test_usage_and_cost_are_accumulated_across_rounds(db):
    client = fake_client(script=[
        {"tool_call": "describe_tables", "arguments": {}},
        {"text": ANSWER, "usage": (1_000, 200)},
    ])
    _, turn = Assistant(client, db).ask("q")
    assert turn.input_tokens > 1_000 and turn.output_tokens >= 200
    assert turn.cost_usd == pytest.approx((turn.input_tokens * 0.20 + turn.output_tokens * 1.20) / 1e6)
