"""Human-in-the-loop approvals: suspend, approve/execute, reject/replan, expiry."""

from __future__ import annotations

import json
import uuid
from datetime import UTC, datetime, timedelta

import httpx
from legate.db.models.approval import Approval
from legate.db.models.workspace import Workspace
from legate.policy.roles import Role


def _call(name, args):
    return json.dumps({"thought": "act", "action": name, "args": args})


def _final(text):
    return json.dumps({"thought": "done", "action": "final", "args": {"output": text}})


def _http_handler(request: httpx.Request) -> httpx.Response:
    return httpx.Response(200, json={"ok": True}, headers={"content-type": "application/json"})


def _setup_http_agent(client, helpers, token):
    helpers.install_connector(
        client,
        token,
        type="http",
        name="API",
        config={"base_url": "https://api.example.com", "auth_type": "bearer"},
        secrets={"token": "t"},
    )
    return helpers.create_agent(client, token, name="Caller", tools=["http.call"])


def _run_until_suspended(
    client, helpers, set_connector_http, set_agent_responses, token, agent, responses
):
    set_connector_http(_http_handler)
    set_agent_responses(responses)
    created = helpers.run_agent(client, token, agent["id"], input="call the api")
    return created["run"]


def test_sensitive_action_suspends_and_creates_approval(
    client, helpers, set_connector_http, set_agent_responses
):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)

    run = _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    assert run["status"] == "waiting_approval"

    inbox = client.get("/approvals", params={"status": "pending"}, headers=helpers.headers(token))
    assert inbox.status_code == 200
    pending = inbox.json()
    assert len(pending) == 1
    assert pending[0]["requested_tool"] == "http.call"
    assert pending[0]["requested_args"] == {"method": "GET", "url": "/status"}


def test_approve_resumes_and_executes_action(
    client, helpers, set_connector_http, set_agent_responses
):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)
    run = _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("all good")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]

    res = client.post(f"/approvals/{approval_id}/approve", headers=helpers.headers(token))
    assert res.status_code == 200
    body = res.json()
    assert body["approval"]["status"] == "approved"
    assert body["run_status"] == "succeeded"

    steps = client.get(f"/runs/{run['id']}/steps", headers=helpers.headers(token)).json()
    ok_calls = [
        s
        for s in steps
        if s["type"] == "tool_call" and s["name"] == "http.call" and s["status"] == "ok"
    ]
    assert ok_calls, "the approved tool should have executed after approval"


def test_reject_lets_agent_replan_without_executing(
    client, helpers, set_connector_http, set_agent_responses
):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)
    run = _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("okay, skipping it")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]

    res = client.post(
        f"/approvals/{approval_id}/reject",
        json={"reason": "not allowed"},
        headers=helpers.headers(token),
    )
    assert res.status_code == 200
    assert res.json()["approval"]["status"] == "rejected"
    assert res.json()["run_status"] == "succeeded"

    steps = client.get(f"/runs/{run['id']}/steps", headers=helpers.headers(token)).json()
    ok_calls = [
        s
        for s in steps
        if s["type"] == "tool_call" and s["name"] == "http.call" and s["status"] == "ok"
    ]
    assert not ok_calls, "a rejected action must never execute"


def test_only_approvers_can_decide(client, helpers, set_connector_http, set_agent_responses):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    builder = helpers.add_member(client, token, email="b@x.com", role=Role.BUILDER)
    operator = helpers.add_member(client, token, email="o@x.com", role=Role.OPERATOR)
    agent = _setup_http_agent(client, helpers, token)
    _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]

    forbidden = client.post(
        f"/approvals/{approval_id}/approve", headers=helpers.headers(builder["access_token"])
    )
    assert forbidden.status_code == 403

    allowed = client.post(
        f"/approvals/{approval_id}/approve", headers=helpers.headers(operator["access_token"])
    )
    assert allowed.status_code == 200


def test_expired_approval_cannot_be_decided(
    client, helpers, set_connector_http, set_agent_responses, session_factory
):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)
    _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]

    session = session_factory()
    approval = session.get(Approval, uuid.UUID(approval_id))
    approval.expires_at = datetime.now(UTC) - timedelta(hours=1)
    session.commit()
    session.close()

    res = client.post(f"/approvals/{approval_id}/approve", headers=helpers.headers(token))
    assert res.status_code == 409
    got = client.get(f"/approvals/{approval_id}", headers=helpers.headers(token)).json()
    assert got["status"] == "expired"


def test_approval_cannot_be_decided_twice(client, helpers, set_connector_http, set_agent_responses):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)
    _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]

    assert (
        client.post(f"/approvals/{approval_id}/approve", headers=helpers.headers(token)).status_code
        == 200
    )
    again = client.post(f"/approvals/{approval_id}/reject", headers=helpers.headers(token))
    assert again.status_code == 409


def test_auto_approve_rule_skips_suspension(
    client, helpers, set_connector_http, set_agent_responses, session_factory
):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]

    session = session_factory()
    workspace = session.get(Workspace, uuid.UUID(owner["workspace"]["id"]))
    workspace.settings_json = {"approval": {"auto_approve": ["http.call"]}}
    session.commit()
    session.close()

    agent = _setup_http_agent(client, helpers, token)
    run = _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    assert run["status"] == "succeeded"  # auto-approved, never paused
    assert client.get("/approvals", headers=helpers.headers(token)).json() == []


def test_approvals_are_tenant_scoped(client, helpers, set_connector_http, set_agent_responses):
    a = helpers.bootstrap_owner(client, email="a@x.com", name="A")
    b = helpers.bootstrap_owner(client, email="b@x.com", name="B")
    agent = _setup_http_agent(client, helpers, a["access_token"])
    _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        a["access_token"],
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(a["access_token"])).json()[0][
        "id"
    ]

    assert (
        client.get(
            f"/approvals/{approval_id}", headers=helpers.headers(b["access_token"])
        ).status_code
        == 404
    )
    assert client.get("/approvals", headers=helpers.headers(b["access_token"])).json() == []


def test_policy_decisions_are_audited(client, helpers, set_connector_http, set_agent_responses):
    owner = helpers.bootstrap_owner(client)
    token = owner["access_token"]
    agent = _setup_http_agent(client, helpers, token)
    _run_until_suspended(
        client,
        helpers,
        set_connector_http,
        set_agent_responses,
        token,
        agent,
        [_call("http.call", {"method": "GET", "url": "/status"}), _final("done")],
    )
    approval_id = client.get("/approvals", headers=helpers.headers(token)).json()[0]["id"]
    client.post(f"/approvals/{approval_id}/approve", headers=helpers.headers(token))

    actions = {e["action"] for e in client.get("/audit", headers=helpers.headers(token)).json()}
    assert "policy.require_approval" in actions
    assert "approval.approve" in actions
