from __future__ import annotations

import json
import os
import subprocess
import time
from pathlib import Path

import httpx
import pytest

from contractlane_operator_sdk import APIError, ClientOptions, OperatorClient
from contractlane_operator_sdk.models import RequestOptions

pytestmark = pytest.mark.integration


REPO_ROOT = Path(__file__).resolve().parents[3]
AGENT_PROOF_HELPER = REPO_ROOT / "contractlane-operator" / "scripts" / "agentproof_helper.go"


class SharedState:
    signup_email: str | None = None
    signup_session_id: str | None = None
    session_token: str | None = None
    session_org_id: str | None = None
    org_id: str | None = None
    project_id: str | None = None
    actor_id: str | None = None
    agent_token: str | None = None
    contract_id: str | None = None
    admin_auth: str | None = None


STATE = SharedState()


def required_env(name: str) -> str:
    value = os.getenv(name, "").strip()
    if not value:
        raise RuntimeError(f"missing required env: {name}")
    return value


def challenge_headers(_: object = None) -> dict[str, str]:
    return {
        "X-Signup-Challenge": required_env("TEST_CHALLENGE_SIGNUP"),
        "X-Operator-Challenge": required_env("TEST_CHALLENGE_OPERATOR"),
        "X-Operator-Challenge-Token": required_env("TEST_CHALLENGE_OPERATOR_TOKEN"),
    }


def request_id_of(response) -> str:
    return str(response.data.get("request_id") or response.meta.request_id or "")


def go_helper(*args: str) -> str:
    cmd = ["go", "run", str(AGENT_PROOF_HELPER), *args]
    return subprocess.check_output(cmd, cwd=REPO_ROOT, text=True).strip()


def test_signup_auth_flow() -> None:
    base_url = required_env("OPERATOR_BASE_URL")
    timeout = float(required_env("TEST_TIMEOUT_SECONDS"))
    email = required_env("TEST_USER_EMAIL").replace("@", f"+py-{int(time.time() * 1000)}@")
    STATE.signup_email = email

    client = OperatorClient(
        ClientOptions(
            base_url=base_url,
            timeout=timeout,
            challenge_headers=challenge_headers,
            httpx_client=httpx.Client(verify=False),
        )
    )

    start = client.public.signup.start({"email": email, "org_name": "SDK PY Org"})
    assert request_id_of(start)
    session_id = str(start.data.get("signup_session", {}).get("session_id", ""))
    assert session_id
    STATE.signup_session_id = session_id

    with pytest.raises(APIError):
        client.public.signup.verify({"session_id": session_id, "verification_code": "000000"})

    verify_code = str(start.data.get("challenge", {}).get("verification_code") or os.getenv("TEST_SIGNUP_VERIFICATION_CODE", ""))
    if not verify_code:
        raise RuntimeError("missing verification code. set TEST_SIGNUP_VERIFICATION_CODE when APP_ENV hides codes")

    verify = client.public.signup.verify({"session_id": session_id, "verification_code": verify_code})
    assert request_id_of(verify)

    complete = client.public.signup.complete(
        {
            "session_id": session_id,
            "project_name": "SDK PY Project",
            "agent_name": "SDK PY Agent",
            "scopes": ["cel.contracts:write", "cel.approvals:decide", "cel.proof:read"],
        }
    )
    assert request_id_of(complete)
    signup_org_id = str(complete.data.get("org", {}).get("org_id", ""))
    if signup_org_id:
        STATE.session_org_id = signup_org_id

    no_challenge = OperatorClient(ClientOptions(base_url=base_url, timeout=timeout, httpx_client=httpx.Client(verify=False)))
    with pytest.raises(APIError):
        no_challenge.public.signup.start({"email": f"{int(time.time())}-nochallenge@example.com", "org_name": "No Challenge"})

    magic_start = client.public.auth.magic_link_start({"email": email})
    link_id = str(magic_start.data.get("magic_link", {}).get("link_id", ""))
    token = str(magic_start.data.get("magic_link", {}).get("token") or os.getenv("TEST_MAGIC_LINK_TOKEN", ""))
    if not link_id or not token:
        raise RuntimeError("missing magic-link link_id/token; set TEST_MAGIC_LINK_TOKEN when token is hidden")

    magic_verify = client.public.auth.magic_link_verify({"link_id": link_id, "token": token})
    session_token = str(magic_verify.data.get("session", {}).get("token", ""))
    if not session_token:
        raise RuntimeError("missing session token from magic-link verify")

    switch_org_id = STATE.session_org_id or str(magic_verify.data.get("session", {}).get("org_id", ""))
    if switch_org_id:
        scoped_client = OperatorClient(
            ClientOptions(
                base_url=base_url,
                timeout=timeout,
                session_token=session_token,
                httpx_client=httpx.Client(verify=False),
            )
        )
        switched = scoped_client.public.auth.switch_org({"org_id": switch_org_id})
        switched_token = str(switched.data.get("session", {}).get("token", ""))
        if switched_token:
            session_token = switched_token

    STATE.session_token = session_token

    session_client = OperatorClient(
        ClientOptions(
            base_url=base_url,
            timeout=timeout,
            session_token=session_token,
            httpx_client=httpx.Client(verify=False),
        )
    )
    session = session_client.public.auth.session()
    assert request_id_of(session)


def test_admin_actor_and_credential_issue_flow() -> None:
    strict_admin = os.getenv("TEST_ADMIN_STRICT_SUCCESS", "") == "1"
    if not STATE.session_token:
        raise RuntimeError("session token not initialized")

    client = OperatorClient(
        ClientOptions(
            base_url=required_env("OPERATOR_BASE_URL"),
            timeout=float(required_env("TEST_TIMEOUT_SECONDS")),
            session_token=STATE.session_token,
            httpx_client=httpx.Client(verify=False),
        )
    )
    bootstrap_token = (os.getenv("TEST_BOOTSTRAP_TOKEN", "") or os.getenv("TEST_UPSTREAM_TOKEN", "")).strip()
    bootstrap_headers = {"X-Operator-User-Email": STATE.signup_email or required_env("TEST_USER_EMAIL")}

    try:
        org = client.operator.admin.create_org(
            {
                "name": f"SDK PY Admin Org {int(time.time())}",
                "admin_email": STATE.signup_email or required_env("TEST_USER_EMAIL"),
            }
        )
        STATE.admin_auth = "session"
    except APIError as exc:
        if exc.status != 401 or not bootstrap_token:
            raise
        bootstrap_client = OperatorClient(
            ClientOptions(
                base_url=required_env("OPERATOR_BASE_URL"),
                timeout=float(required_env("TEST_TIMEOUT_SECONDS")),
                session_token=bootstrap_token,
                httpx_client=httpx.Client(verify=False),
            )
        )
        try:
            org = bootstrap_client.operator.admin.create_org(
                {
                    "name": f"SDK PY Admin Org {int(time.time())}",
                    "admin_email": STATE.signup_email or required_env("TEST_USER_EMAIL"),
                },
                RequestOptions(headers=bootstrap_headers),
            )
        except APIError as bootstrap_exc:
            if bootstrap_exc.status == 401 and not strict_admin:
                return
            raise
        STATE.admin_auth = "bootstrap"
    org_id = str(org.data.get("org", {}).get("org_id", ""))
    assert org_id
    STATE.org_id = org_id

    admin_client = client
    request_options = None
    if STATE.admin_auth == "bootstrap":
        admin_client = OperatorClient(
            ClientOptions(
                base_url=required_env("OPERATOR_BASE_URL"),
                timeout=float(required_env("TEST_TIMEOUT_SECONDS")),
                session_token=bootstrap_token,
                httpx_client=httpx.Client(verify=False),
            )
        )
        request_options = RequestOptions(headers={**bootstrap_headers, "X-Operator-Org-Id": org_id})

    project = admin_client.operator.admin.create_project(
        org_id,
        {"name": f"SDK PY Project {int(time.time())}", "jurisdiction": "US", "timezone": "UTC"},
        request_options,
    )
    project_id = str(project.data.get("project", {}).get("project_id", ""))
    assert project_id
    STATE.project_id = project_id

    actor = admin_client.operator.admin.create_actor(
        project_id,
        {"name": f"SDK PY Actor {int(time.time())}", "scopes": ["cel.contracts:write", "cel.approvals:decide", "cel.proof:read"]},
        request_options,
    )
    actor_id = str(actor.data.get("actor", {}).get("actor_id", ""))
    assert actor_id
    STATE.actor_id = actor_id

    issued = admin_client.operator.admin.issue_credential(
        {
            "actor_id": actor_id,
            "upstream_token": required_env("TEST_UPSTREAM_TOKEN"),
            "scopes": ["cel.contracts:write"],
            "ttl_minutes": 15,
        },
        request_options,
    )
    assert request_id_of(issued)

    listed = admin_client.operator.admin.list_credentials(actor_id, request_options)
    assert request_id_of(listed)



def test_agent_enrollment_flow_including_reject_path() -> None:
    base_url = required_env("OPERATOR_BASE_URL")
    timeout = float(required_env("TEST_TIMEOUT_SECONDS"))

    keygen = json.loads(go_helper("gen"))
    public_jwk = keygen["public_key_jwk"]
    private_key = keygen["private_key"]

    client = OperatorClient(
        ClientOptions(
            base_url=base_url,
            timeout=timeout,
            challenge_headers=challenge_headers,
            httpx_client=httpx.Client(verify=False),
        )
    )

    challenge = client.public.agent_enrollment.challenge({"public_key_jwk": public_jwk})
    challenge_id = str(challenge.data.get("challenge", {}).get("challenge_id", ""))
    nonce = str(challenge.data.get("challenge", {}).get("nonce", ""))
    assert challenge_id and nonce

    with pytest.raises(APIError):
        client.public.agent_enrollment.start(
            {
                "challenge_id": challenge_id,
                "signature": "invalid",
                "sponsor_email": STATE.signup_email or required_env("TEST_USER_EMAIL"),
                "org_name": f"SDK PY Agent Org {int(time.time())}",
            }
        )

    signature = go_helper("sign", "--private-key", private_key, "--challenge-id", challenge_id, "--nonce", nonce)
    start = client.public.agent_enrollment.start(
        {
            "challenge_id": challenge_id,
            "signature": signature,
            "sponsor_email": STATE.signup_email or required_env("TEST_USER_EMAIL"),
            "org_name": f"SDK PY Agent Org {int(time.time())}",
            "requested_scopes": ["cel.contracts:write", "cel.approvals:decide", "cel.proof:read"],
        }
    )

    enrollment_id = str(start.data.get("enrollment", {}).get("enrollment_id", ""))
    approval_token = str(start.data.get("enrollment", {}).get("approval_token") or os.getenv("TEST_AGENT_APPROVAL_TOKEN", ""))
    if not enrollment_id or not approval_token:
        raise RuntimeError("missing enrollment_id/approval_token for approval flow")

    approve = client.public.agent_enrollment.approve(enrollment_id, {"approval_token": approval_token})
    assert request_id_of(approve)

    final_challenge = client.public.agent_enrollment.challenge({"public_key_jwk": public_jwk})
    final_challenge_id = str(final_challenge.data.get("challenge", {}).get("challenge_id", ""))
    final_nonce = str(final_challenge.data.get("challenge", {}).get("nonce", ""))
    final_sig = go_helper("sign", "--private-key", private_key, "--challenge-id", final_challenge_id, "--nonce", final_nonce)

    finalize = client.public.agent_enrollment.finalize(enrollment_id, {"challenge_id": final_challenge_id, "signature": final_sig})
    agent_token = str(finalize.data.get("credential", {}).get("token", ""))
    if not agent_token:
        raise RuntimeError("missing agent credential token after finalize")
    STATE.agent_token = agent_token

    reject_challenge = client.public.agent_enrollment.challenge({"public_key_jwk": public_jwk})
    reject_challenge_id = str(reject_challenge.data.get("challenge", {}).get("challenge_id", ""))
    reject_nonce = str(reject_challenge.data.get("challenge", {}).get("nonce", ""))
    reject_sig = go_helper("sign", "--private-key", private_key, "--challenge-id", reject_challenge_id, "--nonce", reject_nonce)

    reject_start = client.public.agent_enrollment.start(
        {
            "challenge_id": reject_challenge_id,
            "signature": reject_sig,
            "sponsor_email": STATE.signup_email or required_env("TEST_USER_EMAIL"),
            "org_name": f"SDK PY Reject Org {int(time.time())}",
            "requested_scopes": ["cel.contracts:write"],
        }
    )
    reject_enrollment_id = str(reject_start.data.get("enrollment", {}).get("enrollment_id", ""))
    reject_approval_token = str(reject_start.data.get("enrollment", {}).get("approval_token") or os.getenv("TEST_AGENT_APPROVAL_TOKEN", ""))
    client.public.agent_enrollment.reject(reject_enrollment_id, {"approval_token": reject_approval_token})

    with pytest.raises(APIError):
        client.public.agent_enrollment.finalize(reject_enrollment_id, {"challenge_id": reject_challenge_id, "signature": reject_sig})


def test_gateway_contract_flow() -> None:
    if not STATE.agent_token:
        raise RuntimeError("agent token not initialized")

    base_url = required_env("OPERATOR_BASE_URL")
    timeout = float(required_env("TEST_TIMEOUT_SECONDS"))
    client = OperatorClient(
        ClientOptions(
            base_url=base_url,
            timeout=timeout,
            operator_token=STATE.agent_token,
            httpx_client=httpx.Client(verify=False),
        )
    )

    create_body = json.loads(os.getenv("TEST_GATEWAY_CREATE_BODY", "{}"))
    strict_gateway = os.getenv("TEST_GATEWAY_STRICT_SUCCESS", "") == "1"
    try:
        created = client.gateway.cel.create_contract(create_body)
        assert request_id_of(created)
    except APIError as exc:
        if strict_gateway or exc.status != 404:
            raise
        return

    contract_id = str(
        created.data.get("contract_id")
        or created.data.get("contract", {}).get("contract_id")
        or os.getenv("TEST_GATEWAY_CONTRACT_ID", "")
    )
    if not contract_id:
        raise RuntimeError("missing contract_id from create response and TEST_GATEWAY_CONTRACT_ID not set")
    STATE.contract_id = contract_id

    action_name = os.getenv("TEST_GATEWAY_ACTION", "send")
    action_body = json.loads(os.getenv("TEST_GATEWAY_ACTION_BODY", "{}"))
    try:
        action = client.gateway.cel.contract_action(contract_id, action_name, action_body)
        assert request_id_of(action)
    except APIError as exc:
        if strict_gateway or exc.status != 404:
            raise

    try:
        proof = client.gateway.cel.proof_bundle(contract_id)
        assert request_id_of(proof)
    except APIError as exc:
        if strict_gateway or exc.status != 404:
            raise
