"""The Custom Node code editor's Run (POST /tasks/code/run): the same run a
task does, plus problems that point at the failing line and say how to fix it
(app/crud/code_runner.py)."""

import shutil

import pytest

from app.crud import code_runner

READ = "import sys, json\ncontext = json.load(sys.stdin)\n"


async def _run(client, auth_headers, code, **extra):
    resp = await client.post("/tasks/code/run", headers=auth_headers, json={"language": "python", "code": code, **extra})
    assert resp.status_code == 200, resp.text
    return resp.json()


def _errors(result):
    return [p for p in result["problems"] if p["severity"] == "error"]


async def test_success_shows_result_outputs_and_logs(client, auth_headers):
    code = READ + 'print("checking", context["amount"])\nprint(json.dumps({"total": context["amount"] * 2, "currency": "INR"}))'
    result = await _run(
        client, auth_headers, code,
        context={"payment_amount": 50},
        input_params=[{"name": "amount", "type": "number", "source_field": "payment_amount"}],
        output_params=[{"name": "total", "type": "number"}],
    )
    assert result["ok"] is True and result["ran"] is True, result
    assert result["inputs"] == {"amount": 50}
    assert result["result"] == {"total": 100, "currency": "INR"}
    assert result["outputs"] == {"total": 100}
    assert result["logs"] == ["checking 50"]
    assert result["problems"] == []


async def test_syntax_error_points_at_the_line_and_is_not_run(client, auth_headers):
    result = await _run(client, auth_headers, READ + "if context:\n    x = 1\nfor i in range(3)\n    print(i)\n")
    assert result["ran"] is False and result["ok"] is False
    [problem] = _errors(result)
    assert problem["line"] == 5
    assert "':'" in problem["fix"] and "line 5" in problem["fix"]


async def test_indentation_error_says_what_to_indent(client, auth_headers):
    result = await _run(client, auth_headers, READ + "if context:\nprint('x')\n")
    [problem] = _errors(result)
    assert problem["line"] == 4 and "indented" in problem["fix"]


async def test_missing_key_names_the_inputs_that_exist(client, auth_headers):
    code = READ + 'print(json.dumps({"x": context["gateway"]}))'
    result = await _run(client, auth_headers, code, context={"a": 1}, input_params=[{"name": "amount", "type": "any", "source_field": "a"}])
    assert result["ran"] is True and result["ok"] is False
    [problem] = _errors(result)
    assert problem["line"] == 3
    assert "KeyError" in problem["message"] and "'gateway'" in problem["fix"] and "amount" in problem["fix"]
    assert "Traceback" in result["stderr"] and "script.py" in result["stderr"]
    assert "Temp" not in result["stderr"] and "tmp" not in result["stderr"]  # no server paths


async def test_uninstalled_module(client, auth_headers):
    result = await _run(client, auth_headers, "import definitely_not_installed_xyz\n")
    [problem] = _errors(result)
    assert problem["line"] == 1 and "isn't installed" in problem["fix"]


async def test_declared_output_missing_from_result(client, auth_headers):
    result = await _run(client, auth_headers, READ + 'print(json.dumps({"status": "ok"}))', output_params=[{"name": "receipt_no", "type": "string"}])
    assert result["ok"] is False and result["outputs"] is None
    [problem] = _errors(result)
    assert "receipt_no" in problem["message"] and "status" in problem["fix"]


async def test_non_json_last_line_is_a_warning(client, auth_headers):
    result = await _run(client, auth_headers, READ + 'print("done")')
    assert result["ok"] is True and result["result"] == "done\n"
    assert [p["severity"] for p in result["problems"]] == ["warning"]
    assert "json.dumps" in result["problems"][0]["fix"]


async def test_missing_required_input_is_reported_before_running(client, auth_headers):
    result = await _run(client, auth_headers, READ + "print(1)", input_params=[{"name": "amount", "type": "number", "source_field": "payment_amount"}])
    assert result["ran"] is False
    [problem] = _errors(result)
    assert "amount" in problem["message"] and "Test input" in problem["fix"]


async def test_script_that_ignores_its_inputs_gets_a_hint(client, auth_headers):
    result = await _run(client, auth_headers, 'import json\nprint(json.dumps({"ok": True}))', context={"x": 1}, input_params=[{"name": "x", "type": "any", "source_field": "x"}])
    assert result["ok"] is True
    assert [p["severity"] for p in result["problems"]] == ["info"]


async def test_timeout(client, auth_headers, monkeypatch):
    monkeypatch.setattr(code_runner, "TIMEOUT_SECONDS", 1)
    result = await _run(client, auth_headers, "while True:\n    pass\n")
    [problem] = _errors(result)
    assert "longer than 1 seconds" in problem["message"] and "loop" in problem["fix"]


@pytest.mark.skipif(shutil.which("node") is None, reason="node not installed")
async def test_node_reference_error_points_at_the_line(client, auth_headers):
    code = 'let input = "";\nprocess.stdin.on("data", (d) => (input += d));\nprocess.stdin.on("end", () => {\n  const ctx = JSON.parse(input);\n  console.log(JSON.stringify({ total: amount * 2 }));\n});'
    resp = await client.post(
        "/tasks/code/run", headers=auth_headers,
        json={"language": "node", "code": code, "context": {"a": 5}, "input_params": [{"name": "amount", "type": "number", "source_field": "a"}]},
    )
    result = resp.json()
    [problem] = _errors(result)
    assert problem["line"] == 5 and "ctx.amount" in problem["fix"]


@pytest.mark.skipif(shutil.which("node") is None, reason="node not installed")
async def test_node_script_runs(client, auth_headers):
    code = 'let input = "";\nprocess.stdin.on("data", (d) => (input += d));\nprocess.stdin.on("end", () => {\n  const ctx = JSON.parse(input);\n  console.log("got", ctx.amount);\n  console.log(JSON.stringify({ total: ctx.amount * 2 }));\n});'
    resp = await client.post(
        "/tasks/code/run", headers=auth_headers,
        json={"language": "node", "code": code, "context": {"a": 5}, "input_params": [{"name": "amount", "type": "number", "source_field": "a"}], "output_params": [{"name": "total", "type": "number"}]},
    )
    result = resp.json()
    assert result["ok"] is True, result
    assert result["outputs"] == {"total": 10} and result["logs"] == ["got 5"]


async def test_vendors_cannot_run_code(client, test_tenant):
    import uuid

    from app.core.auth import create_access_token

    token = create_access_token(tenant_id=test_tenant["tenant_id"], user_id=test_tenant["admin_id"], vendor_id=uuid.uuid4())
    resp = await client.post("/tasks/code/run", headers={"Authorization": f"Bearer {token}"}, json={"code": "print(1)"})
    assert resp.status_code in (401, 403)
