import assert from "node:assert/strict";
import { setTimeout as delay } from "node:timers/promises";
import postgres from "postgres";

import { loginBrowser, requireOperatorPassword } from "./auth.js";
import { ownerCreatedJourneyEnvironment, requiredEnvironment } from "./exact-journey.js";

const webUrl = requiredEnvironment(process.env, "AWP_WEB_URL");
const databaseUrl = requiredEnvironment(process.env, "AWP_TEST_POSTGRES_URL");
const playwrightModule = requiredEnvironment(process.env, "AWP_PLAYWRIGHT_MODULE");
const operatorPassword = requireOperatorPassword();
const { projectId, planRevisionId, taskIds } = ownerCreatedJourneyEnvironment();
const firstTaskId = taskIds[0];
const timeoutMs = Number(process.env.AWP_GOLIVE_TIMEOUT_MS ?? "240000");
const sql = postgres(databaseUrl, { max: 1 });

type EventRow = {
  type: string;
  aggregate_id: string;
  occurred_at: Date;
};

async function waitForEvent(query: () => Promise<EventRow[]>): Promise<EventRow> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const rows = await query();
    if (rows.length > 1) throw new Error("canonical live transition matched more than one event");
    if (rows[0]) return rows[0];
    await delay(100);
  }
  throw new Error("timed out waiting for canonical live transition event");
}

const alreadyDispatched = await sql<{ count: number }[]>`
  select count(*)::int as count
  from business_events
  where type = 'TaskDispatched'
    and aggregate_type = 'Task'
    and aggregate_id = ${firstTaskId}
    and project_id = ${projectId}
`;
assert.equal(
  alreadyDispatched[0]?.count,
  0,
  "AC-23 observer must start before owner approval dispatches the first Task",
);

const { chromium } = await import(playwrightModule);
const browser = await chromium.launch({ headless: true });
try {
  const page = await browser.newPage();
  await loginBrowser(page, webUrl, operatorPassword);
  await page.goto(`${webUrl}/projects/${encodeURIComponent(projectId)}`, {
    waitUntil: "networkidle",
  });
  const observed: string[] = [];

  async function requireVisibleWithinFiveSeconds(event: EventRow): Promise<void> {
    const locator = page.locator(
      `[data-live-stage-event="${event.type}"][data-event-aggregate-id="${event.aggregate_id}"]`,
    );
    await locator.waitFor({ timeout: 5_000 });
    const visibleAt = Date.now();
    const occurredAt = event.occurred_at.getTime();
    assert.ok(
      visibleAt - occurredAt <= 5_000,
      `${event.type} became visible ${visibleAt - occurredAt}ms after its durable occurrence`,
    );
    observed.push(event.type);
  }

  const taskDispatched = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select type, aggregate_id, occurred_at
      from business_events
      where type = 'TaskDispatched'
        and aggregate_type = 'Task'
        and aggregate_id = ${firstTaskId}
        and project_id = ${projectId}
        and payload->>'planRevisionId' = ${planRevisionId}
      order by occurred_at, id
    `,
  );
  await requireVisibleWithinFiveSeconds(taskDispatched);

  const factoryRunStarted = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select e.type, e.aggregate_id, e.occurred_at
      from business_events e
      join factory_runs fr on fr.id = e.aggregate_id
      where e.type = 'FactoryRunStarted'
        and e.aggregate_type = 'FactoryRun'
        and e.project_id = ${projectId}
        and fr.plan_revision_id = ${planRevisionId}
        and fr.task_id = ${firstTaskId}
        and e.payload->>'taskId' = ${firstTaskId}
      order by e.occurred_at, e.id
    `,
  );
  await requireVisibleWithinFiveSeconds(factoryRunStarted);

  const agentRunStarted = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select e.type, e.aggregate_id, e.occurred_at
      from business_events e
      join agent_runs ar on ar.id = e.aggregate_id
      where e.type = 'AgentRunStarted'
        and e.aggregate_type = 'AgentRun'
        and e.project_id = ${projectId}
        and ar.factory_run_id = ${factoryRunStarted.aggregate_id}
        and ar.task_id = ${firstTaskId}
        and ar.role = 'coder'
      order by e.occurred_at, e.id
    `,
  );
  await requireVisibleWithinFiveSeconds(agentRunStarted);

  const changeSetCreated = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select e.type, e.aggregate_id, e.occurred_at
      from business_events e
      join change_sets cs on cs.id = e.aggregate_id
      join attempts a on a.id = cs.producer_attempt_id
      join agent_runs ar on ar.id = a.agent_run_id
      where e.type = 'ChangeSetCreated'
        and e.aggregate_type = 'ChangeSet'
        and e.project_id = ${projectId}
        and ar.factory_run_id = ${factoryRunStarted.aggregate_id}
        and cs.task_id = ${firstTaskId}
      order by e.occurred_at, e.id
    `,
  );
  await requireVisibleWithinFiveSeconds(changeSetCreated);

  const reviewRecorded = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select e.type, e.aggregate_id, e.occurred_at
      from business_events e
      join reviews r on r.id = e.aggregate_id
      where e.type = 'ReviewAgentAssigned'
        and e.aggregate_type = 'Review'
        and e.project_id = ${projectId}
        and r.change_set_id = ${changeSetCreated.aggregate_id}
      order by e.occurred_at, e.id
    `,
  );
  await requireVisibleWithinFiveSeconds(reviewRecorded);

  const mergeCompleted = await waitForEvent(
    () =>
      sql<EventRow[]>`
      select type, aggregate_id, occurred_at
      from business_events
      where type = 'MergeCompleted'
        and aggregate_type = 'ChangeSet'
        and aggregate_id = ${changeSetCreated.aggregate_id}
        and project_id = ${projectId}
      order by occurred_at, id
    `,
  );
  await requireVisibleWithinFiveSeconds(mergeCompleted);

  assert.deepEqual(observed, [
    "TaskDispatched",
    "FactoryRunStarted",
    "AgentRunStarted",
    "ChangeSetCreated",
    "ReviewAgentAssigned",
    "MergeCompleted",
  ]);
  process.stdout.write(
    `AC-23 PASS: one open canonical Project page observed and retained all six authoritative first-Task transitions within five seconds, without reload\n`,
  );
} finally {
  await browser.close();
  await sql.end({ timeout: 5 });
}
