"""The full sol-web tool loop, driven against a scripted browser.

Nothing here spends a real ChatGPT turn.
"""

import json

import pytest

import chat
import protocol
import solwebd
import translate
from fake_browser import block, seat_for

TOOLS = [{"name": "run_command", "description": "run a shell command",
          "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}}}]
SYSTEM = {"role": "system", "content": "be terse"}
ASK = {"role": "user", "content": "count the py files"}

CALL = block(json.dumps({"tool": "run_command", "arguments": {"command": "ls | wc -l"}}))
DONE = block(json.dumps({"tool": "final", "text": "8"}))


def request(messages, model="sol-web-medium", **extra):
    return {"model": model, "messages": messages, "tools": TOOLS, **extra}


@pytest.fixture(autouse=True)
def instant_waits(monkeypatch):
    monkeypatch.setattr(chat.time, "sleep", lambda *_: None)


def engine_for(replies):
    seat, marionette = seat_for(replies)
    return solwebd.Engine(seat), marionette


def test_two_step_loop_returns_a_tool_call_then_a_final_answer():
    engine, marionette = engine_for([CALL, DONE])
    first = engine.complete(request([SYSTEM, ASK]))
    assert first["choices"][0]["finish_reason"] == "tool_calls"
    call = first["choices"][0]["message"]["tool_calls"][0]
    assert call["function"]["name"] == "run_command"
    assert json.loads(call["function"]["arguments"]) == {"command": "ls | wc -l"}

    second = engine.complete(request([
        SYSTEM, ASK,
        {"role": "assistant", "content": None, "tool_calls": [call]},
        {"role": "tool", "tool_call_id": call["id"], "content": "8"},
    ]))
    assert second["choices"][0]["finish_reason"] == "stop"
    assert second["choices"][0]["message"]["content"] == "8"
    assert len(marionette.navigations) == 2, "the second turn must reuse the conversation"


def test_the_follow_up_turn_types_only_the_delta():
    engine, marionette = engine_for([CALL, DONE])
    first = engine.complete(request([SYSTEM, ASK]))
    call = first["choices"][0]["message"]["tool_calls"][0]
    engine.complete(request([
        SYSTEM, ASK,
        {"role": "assistant", "content": None, "tool_calls": [call]},
        {"role": "tool", "tool_call_id": call["id"], "content": "8"},
    ]))
    assert marionette.typed[1] == f"TOOL RESULT [{call['id']}]\n8"
    assert "be terse" not in marionette.typed[1]


def test_the_first_turn_carries_the_preamble_and_the_tools():
    engine, marionette = engine_for([DONE])
    reply = engine.complete(request([SYSTEM, ASK]))
    assert "EXACTLY ONE fenced json block" in marionette.typed[0]
    assert "run_command" in marionette.typed[0]
    assert marionette.typed[0].rstrip().endswith("USER\ncount the py files")

    preamble = protocol.render_preamble("be terse", TOOLS) + "\n\n"
    expected = translate.estimate_input([ASK])
    expected += translate.estimate_input([], TOOLS, rendered=preamble)
    assert reply["usage"]["prompt_tokens"] == expected


def test_tool_block_counts_as_output_when_reply_text_omits_it():
    payload = json.dumps({"tool": "final", "text": "8"})
    visible = "completed tool response"
    engine, _ = engine_for([{"blocks": [payload], "text": visible}])
    reply = engine.complete(request([SYSTEM, ASK]))
    expected = len(f"{visible}\n{payload}".encode())
    assert reply["usage"]["completion_tokens"] == expected


@pytest.mark.parametrize("model,label", [("sol-web-high", "high"),
                                         ("sol-web-xhigh", "extra high"),
                                         ("sol-web-pro", "pro")])
def test_effort_is_asserted_before_every_send(model, label):
    engine, marionette = engine_for([CALL, DONE])
    first = engine.complete(request([SYSTEM, ASK], model=model))
    call = first["choices"][0]["message"]["tool_calls"][0]
    engine.complete(request([
        SYSTEM, ASK,
        {"role": "assistant", "content": None, "tool_calls": [call]},
        {"role": "tool", "tool_call_id": call["id"], "content": "8"},
    ], model=model))
    assert marionette.efforts == [label, label]


def test_unknown_model_suffix_is_rejected():
    engine, _ = engine_for([DONE])
    with pytest.raises(ValueError):
        engine.complete(request([SYSTEM, ASK], model="sol-web-turbo"))


def test_appending_to_history_is_a_continuation_not_a_replay():
    engine, marionette = engine_for([CALL, DONE])
    engine.complete(request([SYSTEM, ASK]))
    engine.complete(request([SYSTEM, ASK, {"role": "user", "content": "and the md files"}]))
    assert len(marionette.navigations) == 2, "an append must reuse the conversation"
    assert marionette.typed[1] == "USER\nand the md files"


def test_rewritten_history_opens_a_fresh_conversation_and_replays():
    engine, marionette = engine_for([CALL, DONE, DONE])
    first = engine.complete(request([SYSTEM, ASK]))
    call = first["choices"][0]["message"]["tool_calls"][0]
    delivered = [SYSTEM, ASK,
                 {"role": "assistant", "content": None, "tool_calls": [call]},
                 {"role": "tool", "tool_call_id": call["id"], "content": "8"}]
    engine.complete(request(delivered))

    # The client compacted: a message it already sent now reads differently.
    rewritten = list(delivered)
    rewritten[3] = {"role": "tool", "tool_call_id": call["id"], "content": "(compacted)"}
    engine.complete(request(rewritten + [{"role": "user", "content": "carry on"}]))

    assert len(marionette.navigations) == 3, "a replay must open a new chat"
    assert "count the py files" in marionette.typed[2], "the replay resends the history"
    assert "(compacted)" in marionette.typed[2]
    assert "carry on" in marionette.typed[2]


def test_one_malformed_reply_gets_exactly_one_correction_turn():
    engine, marionette = engine_for([block("not json at all"), DONE])
    reply = engine.complete(request([SYSTEM, ASK]))
    assert reply["choices"][0]["message"]["content"] == "8"
    assert "broke the protocol" in marionette.typed[1]


def test_a_second_malformed_reply_fails_the_request():
    engine, _ = engine_for([block("nope"), block("still nope")])
    with pytest.raises(solwebd.ProtocolBroken):
        engine.complete(request([SYSTEM, ASK]))


def test_a_capped_session_raises_with_a_resume_time():
    engine, marionette = engine_for([DONE])
    marionette.cap = {"text": "You've hit your usage limit. Try again at 3:45 PM."}
    with pytest.raises(solwebd.Capped) as caught:
        engine.complete(request([SYSTEM, ASK]))
    assert caught.value.state["resume_at"] is None or "T" in caught.value.state["resume_at"]


def test_health_reports_conversations_and_an_empty_queue():
    engine, _ = engine_for([DONE])
    engine.complete(request([SYSTEM, ASK]))
    health = engine.health()
    assert health["ok"] and health["conversations"] == 1 and health["queue"] == 0


def test_keepalive_is_a_well_formed_chunk_not_an_sse_comment():
    first = solwebd.keepalive_chunk("sol-web-medium", first=True)
    assert first["object"] == "chat.completion.chunk"
    assert first["choices"][0]["delta"] == {"role": "assistant", "content": ""}
    assert first["choices"][0]["finish_reason"] is None
    assert solwebd.keepalive_chunk("sol-web-medium")["choices"][0]["delta"] == {"content": ""}


def test_stream_chunk_carries_the_whole_answer_and_the_finish_reason():
    engine, _ = engine_for([DONE])
    chunk = solwebd.as_chunk(engine.complete(request([SYSTEM, ASK], stream=True)))
    assert chunk["object"] == "chat.completion.chunk"
    assert chunk["choices"][0]["delta"]["content"] == "8"
    assert chunk["choices"][0]["finish_reason"] == "stop"
