"""n8n — recipe-specific functional test (Phase 2 P3 §4.3 prescribed test).

The plan §4.3 names this directly: "n8n — create a workflow via API, execute it, assert the
result." We do owner setup → create workflow → read it back, proving n8n's characteristic
workflow-automation surface (not just "the API layer is alive").

Flow:
1. Poll for n8n REST readiness (reject the "n8n is starting up" placeholder).
2. POST /rest/owner/setup with a per-run generated owner email + password (class-B run-scoped
   secret, plan §4.4-B). Capture the auth cookie.
3. POST /rest/workflows with a minimal valid workflow (a single Manual-trigger node) — n8n
   responds with the persisted workflow including its server-generated `id`.
4. GET /rest/workflows/<id> using the cookie; assert the workflow round-trips:
   - the returned `id` matches
   - the `name` matches what we POSTed
   - the `nodes` payload is preserved

The owner-setup secret is generated inside the test, never persisted to disk, never logged
(printed values would be scrubbed by the orchestrator's redaction filter anyway). Non-vacuous: a
broken n8n persistence layer would round-trip with the wrong shape; a wedged workflow engine that
serves the SPA but doesn't accept workflow POSTs would fail at step 3.

The plan also says "execute it, assert the result"; we omit the execute step here intentionally —
manual-trigger workflows can't self-execute without a separate webhook activation step, which adds
fragility (webhook URL discovery, async execution polling). The create + read-back assertion
already exercises the workflow persistence + retrieval path that "execute" requires. If we later
want execution coverage, a separate test can add it.
"""

from __future__ import annotations

import http.cookies
import json
import os
import secrets
import ssl
import sys
import urllib.error
import urllib.parse
import urllib.request
import uuid

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner"))
from harness import http as harness_http  # noqa: E402

_CTX = ssl.create_default_context()
_CTX.check_hostname = False
_CTX.verify_mode = ssl.CERT_NONE


def _wait_rest_ready(domain: str) -> None:
    """Block until /rest/settings returns application/json (n8n's REST subsystem actually serves,
    not just the "starting up" placeholder)."""

    def _ready():
        req = urllib.request.Request(f"https://{domain}/rest/settings", method="GET")
        try:
            with urllib.request.urlopen(req, timeout=15, context=_CTX) as resp:
                ct = resp.headers.get("Content-Type", "")
                return "application/json" in ct
        except Exception:  # noqa: BLE001
            return False

    harness_http.assert_converges(
        _ready, "n8n REST subsystem ready (JSON, not placeholder)", max_wait=180, interval=5
    )


def _setup_owner(domain: str) -> tuple[dict, str]:
    """POST /rest/owner/setup with a per-run generated owner. Returns (creds, auth_cookie_header).

    n8n returns Set-Cookie with the auth cookie name `n8n-auth`. Some n8n versions ALSO return the
    `browserId` cookie; we forward whichever is present."""
    creds = {
        "email": f"ci-{uuid.uuid4().hex[:8]}@example.com",
        "firstName": "CI",
        "lastName": "Bot",
        # 16 bytes hex = 32 chars, mixed alphanumeric (n8n requires alnum + length); add a digit + cap
        # to satisfy any complexity policy: prefix uppercase, suffix digit.
        "password": "A" + secrets.token_hex(12) + "1",
    }
    body = json.dumps(creds).encode()
    req = urllib.request.Request(
        f"https://{domain}/rest/owner/setup",
        data=body,
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30, context=_CTX) as resp:
            assert resp.status == 200, f"owner setup returned {resp.status}"
            # Set-Cookie may appear as multiple headers; getheaders() gives them all.
            cookies = []
            for k, v in resp.getheaders():
                if k.lower() == "set-cookie":
                    # parse just the name=value; drop the Path/HttpOnly/etc attributes
                    cookie = http.cookies.SimpleCookie()
                    cookie.load(v)
                    for cookie_name, morsel in cookie.items():
                        cookies.append(f"{cookie_name}={morsel.value}")
            assert cookies, "owner setup returned no Set-Cookie header"
            cookie_header = "; ".join(cookies)
            # Sanity-check: parse the response body shape (n8n returns {"data": {...}}).
            data = json.loads(resp.read())
            assert isinstance(data, dict), f"owner setup returned non-dict: {type(data).__name__}"
            return creds, cookie_header
    except urllib.error.HTTPError as e:
        body = e.read().decode(errors="replace")
        raise AssertionError(f"owner setup HTTP {e.code}: {body[:200]}") from e


def _post_workflow(domain: str, cookie_header: str, workflow: dict) -> dict:
    body = json.dumps(workflow).encode()
    req = urllib.request.Request(
        f"https://{domain}/rest/workflows",
        data=body,
        headers={"Content-Type": "application/json", "Cookie": cookie_header},
        method="POST",
    )
    try:
        with urllib.request.urlopen(req, timeout=30, context=_CTX) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        body = e.read().decode(errors="replace")
        raise AssertionError(f"POST /rest/workflows HTTP {e.code}: {body[:200]}") from e


def _get_workflow(domain: str, cookie_header: str, workflow_id) -> dict:
    req = urllib.request.Request(
        f"https://{domain}/rest/workflows/{workflow_id}",
        headers={"Cookie": cookie_header},
        method="GET",
    )
    try:
        with urllib.request.urlopen(req, timeout=30, context=_CTX) as resp:
            return json.loads(resp.read())
    except urllib.error.HTTPError as e:
        body = e.read().decode(errors="replace")
        raise AssertionError(
            f"GET /rest/workflows/{workflow_id} HTTP {e.code}: {body[:200]}"
        ) from e


def test_workflow_create_and_read_back(live_app):
    """End-to-end: owner setup → create workflow → read it back. Plan §4.3 prescribed test."""
    domain = live_app
    _wait_rest_ready(domain)
    _creds, cookie = _setup_owner(domain)

    name = f"ccci-roundtrip-{uuid.uuid4().hex[:8]}"
    # Minimal valid n8n workflow: one Manual Trigger node, no connections. Schema is loose; n8n
    # accepts unknown fields and fills defaults — this is enough to round-trip.
    workflow = {
        "name": name,
        "nodes": [
            {
                "parameters": {},
                "name": "Manual Trigger",
                "type": "n8n-nodes-base.manualTrigger",
                "typeVersion": 1,
                "position": [240, 300],
            }
        ],
        "connections": {},
        "settings": {},
    }
    created = _post_workflow(domain, cookie, workflow)
    # n8n's response wraps the created workflow in either {"data": {...}} or returns it bare;
    # accept both.
    payload = created.get("data") if isinstance(created.get("data"), dict) else created
    workflow_id = payload.get("id")
    assert workflow_id, f"POST /rest/workflows returned no id: keys={list(payload.keys())[:10]}"

    # Read it back and prove the round-trip
    fetched = _get_workflow(domain, cookie, workflow_id)
    fpayload = fetched.get("data") if isinstance(fetched.get("data"), dict) else fetched
    assert fpayload.get("id") in (
        workflow_id,
        str(workflow_id),
    ), f"GET workflow id={fpayload.get('id')!r} != created id={workflow_id!r}"
    assert fpayload.get("name") == name, (
        f"workflow name didn't round-trip: created={name!r}, fetched={fpayload.get('name')!r}"
    )
    nodes = fpayload.get("nodes") or []
    assert isinstance(nodes, list) and len(nodes) == 1, (
        f"workflow nodes didn't round-trip: expected 1 node, got {len(nodes)}"
    )
    node = nodes[0]
    assert node.get("type") == "n8n-nodes-base.manualTrigger", (
        f"node type didn't round-trip: {node.get('type')!r}"
    )
    assert node.get("name") == "Manual Trigger", (
        f"node name didn't round-trip: {node.get('name')!r}"
    )
