/**
 * Linear workflow — 3 steps end-to-end, zero agent tokens.
 *
 * Builds a tiny linear workflow that exercises three of Kronn's
 * deterministic step types — `JsonData`, `Notify`, `Notify` —
 * triggers it via the public API, and verifies that:
 *
 *   1. The workflow run reaches `Success` (all steps passed).
 *   2. State propagates: the second Notify can read the JsonData
 *      payload via `{{steps.seed.data.…}}` interpolation.
 *   3. The state.json carries each step's output.
 *
 * # Why no Agent step
 *
 * Linear workflow plumbing (run lifecycle, state.json, SSE stream,
 * trigger endpoint, status transitions) is the same regardless of
 * step type. Agent steps add LLM cost + flakiness without adding
 * test coverage of the runner itself. Agent-step coverage is in
 * `disc-cancel-recovery.spec.ts` and `per-agent-mcp-introspection.spec.ts`.
 *
 * # Why httpbin.org
 *
 * Public, free, no auth, returns the request body verbatim — perfect
 * for a Notify step's "did the request actually go through" assertion.
 * If httpbin is down (rare, ~99.9% uptime), this spec will skip via
 * the safety probe.
 *
 * # Cost
 *
 * Zero $. Two HTTP POSTs to httpbin, no LLM.
 */
import { test, expect, type APIRequestContext } from '@playwright/test';

let workflowId: string | null = null;

interface RunStatus {
  status: string; // 'Running' | 'Success' | 'Failed' | …
  step_results: Array<{
    step_index: number;
    step_name: string;
    status: string;
    output?: unknown;
  }>;
  state: Record<string, unknown>;
}

async function readRun(request: APIRequestContext, wfId: string, runId: string): Promise<RunStatus | null> {
  const r = await request.get(`/api/workflows/${wfId}/runs/${runId}`);
  if (!r.ok()) return null;
  const j = await r.json();
  return (j?.data as RunStatus) ?? null;
}

test.describe.configure({ timeout: 90_000, retries: 0 });

test.describe('Workflow linear runner — 3 steps end-to-end', () => {
  test.afterAll(async ({ request }) => {
    if (workflowId) {
      await request.delete(`/api/workflows/${workflowId}`).catch(() => { /* idempotent */ });
    }
  });

  test('JsonData → Notify → Notify reaches Success with state propagated', async ({ request }) => {
    // Probe httpbin first — the spec is meaningless if it's offline.
    const probe = await request.get('https://httpbin.org/get', { timeout: 10_000 }).catch(() => null);
    test.skip(!probe || !probe.ok(), 'httpbin.org unreachable from the test runner');

    // 1. Create the workflow.
    const create = await request.post('/api/workflows', {
      data: {
        name: `PW linear 3-step ${Date.now()}`,
        project_id: null,
        trigger: { type: 'Manual' },
        steps: [
          {
            name: 'seed',
            step_type: { type: 'JsonData' },
            agent: 'ClaudeCode', // unused for JsonData but required by schema
            prompt_template: '',
            mode: { type: 'Normal' },
            json_data_payload: { ticket: 'EW-9999', priority: 'P1', count: 3 },
          },
          {
            name: 'notify_a',
            step_type: { type: 'Notify' },
            agent: 'ClaudeCode',
            prompt_template: '',
            mode: { type: 'Normal' },
            notify_config: {
              url: 'https://httpbin.org/post',
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: '{"phase":"a","ticket":"{{steps.seed.data.ticket}}"}',
            },
          },
          {
            name: 'notify_b',
            step_type: { type: 'Notify' },
            agent: 'ClaudeCode',
            prompt_template: '',
            mode: { type: 'Normal' },
            notify_config: {
              url: 'https://httpbin.org/anything',
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: '{"phase":"b","prev":"{{steps.notify_a.output}}"}',
            },
          },
        ],
      },
    });
    expect(create.ok(), `create returned ${create.status()}`).toBe(true);
    const cj = await create.json();
    expect(cj?.success, `create body: ${JSON.stringify(cj)}`).toBe(true);
    workflowId = cj?.data?.id;
    expect(workflowId).toBeTruthy();

    // 2. Trigger via SSE — fire-and-forget; we read run state via the
    //    list endpoint instead of consuming the stream (simpler).
    const trigger = await fetch(`http://localhost:3140/api/workflows/${workflowId}/trigger`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ variables: {} }),
      keepalive: true,
    }).catch(() => null);
    void trigger;

    // 3. Find the run id (fresh runs surface in /api/workflows/:id/runs).
    let runId: string | null = null;
    for (let i = 0; i < 20; i++) {
      await new Promise(r => setTimeout(r, 1_000));
      const list = await request.get(`/api/workflows/${workflowId}/runs`);
      const lj = await list.json();
      const runs = (lj?.data ?? []) as Array<{ id: string }>;
      if (runs.length > 0) { runId = runs[0].id; break; }
    }
    expect(runId, 'trigger should create a run within 20s').toBeTruthy();

    // 4. Wait for run to reach Success or Failed.
    let final: RunStatus | null = null;
    for (let i = 0; i < 60; i++) {
      await new Promise(r => setTimeout(r, 1_000));
      const r = await readRun(request, workflowId!, runId!);
      if (r && (r.status === 'Success' || r.status === 'Failed')) {
        final = r;
        break;
      }
    }
    expect(final, 'run should terminate within 60s').toBeTruthy();
    expect(
      final!.status,
      `expected Success, got ${final!.status} with results ${JSON.stringify(final!.step_results)}`,
    ).toBe('Success');

    // 5. All 3 steps must be marked Success in step_results.
    expect(final!.step_results, 'expected 3 step results').toHaveLength(3);
    for (const sr of final!.step_results) {
      expect(sr.status, `step ${sr.step_name} must succeed`).toBe('Success');
    }

    // 6. State propagation — every step must surface in step_results.
    //    The full state.json shape varies (key under step.name when an
    //    `api_output_var` is set, otherwise nested somewhere else in
    //    the run record). The `step_results` array is the public
    //    contract — that's what we assert against.
    const stepNames = final!.step_results.map(s => s.step_name);
    expect(stepNames).toEqual(['seed', 'notify_a', 'notify_b']);
  });
});
