import assert from 'node:assert/strict';
import { mkdirSync, writeFileSync } from 'node:fs';
import { createServer, type Server } from 'node:http';
import path from 'node:path';
import test from 'node:test';
import { type CliJsonResult, formatResultDebug, runBuiltCliJson } from './cli-json.ts';
import { assertPngFile } from './provider-scenarios/assertions.ts';

const TEST_NAME = 'live web platform e2e smoke';
const WEB_E2E_ENABLED = process.env.AGENT_DEVICE_WEB_E2E === '1';

type StepRecord = {
  step: string;
  command: string;
  status: number;
  timestamp: string;
  errorCode?: string;
  errorMessage?: string;
};

type WebSmokeContext = {
  artifactDir: string;
  common: string[];
  env: NodeJS.ProcessEnv;
  lastSnapshot?: any;
  screenshotPath: string;
  server: Server;
  stepHistory: StepRecord[];
  url: string;
};

test(
  TEST_NAME,
  {
    skip: WEB_E2E_ENABLED
      ? false
      : 'Set AGENT_DEVICE_WEB_E2E=1 to run the managed web backend smoke.',
  },
  async () => {
    await runWebSmoke(await createWebSmokeContext());
  },
);

async function runWebSmoke(context: WebSmokeContext): Promise<void> {
  let opened = false;

  try {
    await runStep(context, 'set up managed web backend', ['web', 'setup', '--json']);
    await runStep(context, 'verify managed web backend', ['web', 'doctor', '--json']);
    await runStep(context, 'open local fixture', ['open', context.url, ...context.common]);
    opened = true;
    await assertInitialWebSurface(context);
    await assertWebNetwork(context);
    await assertReadAndVisibility(context);
    await assertWebInteractions(context);
    await assertWebScreenshot(context);
  } finally {
    await cleanupWebSmoke(context, opened);
  }
}

async function createWebSmokeContext(): Promise<WebSmokeContext> {
  const artifactDir = createArtifactDir();
  const stateDir = path.join(artifactDir, 'agent-device-state');
  const agentBrowserConfigPath = path.join(artifactDir, 'agent-browser.json');
  const session = `ws-${process.pid.toString(36)}-${(Date.now() % 1_679_616).toString(36)}`;
  const fixture = await startFixtureServer();
  const env = {
    ...process.env,
    AGENT_DEVICE_STATE_DIR: stateDir,
    AGENT_BROWSER_CONFIG: agentBrowserConfigPath,
    AGENT_BROWSER_HEADED: 'false',
    AGENT_BROWSER_IDLE_TIMEOUT_MS: '30000',
  };

  mkdirSync(stateDir, { recursive: true });
  writeFileSync(agentBrowserConfigPath, JSON.stringify({ headed: false }, null, 2));

  return {
    artifactDir,
    common: ['--platform', 'web', '--session', session, '--json'],
    env,
    screenshotPath: path.join(artifactDir, 'web-smoke.png'),
    server: fixture.server,
    stepHistory: [],
    url: fixture.url,
  };
}

async function assertInitialWebSurface(context: WebSmokeContext): Promise<void> {
  const snapshot = await runStep(context, 'capture interactive snapshot', [
    'snapshot',
    '-i',
    ...context.common,
  ]);
  const labels = readSnapshotLabels(snapshot.json);
  assert.ok(labels.includes('Ready marker'), `snapshot labels: ${labels.join(', ')}`);
  assert.ok(labels.includes('Email'), `snapshot labels: ${labels.join(', ')}`);
  assert.ok(labels.includes('Submit order'), `snapshot labels: ${labels.join(', ')}`);
}

async function assertReadAndVisibility(context: WebSmokeContext): Promise<void> {
  await assertCommandData(
    context,
    'wait for ready text',
    ['wait', 'text', 'Ready marker', '5000'],
    {
      text: 'Ready marker',
    },
  );
  await assertCommandData(
    context,
    'read ready text through selector',
    ['get', 'text', 'label="Ready marker"'],
    { text: 'Ready marker' },
  );
  await assertCommandData(
    context,
    'assert submit visible',
    ['is', 'visible', 'label="Submit order"'],
    { pass: true },
  );
  await assertCommandData(
    context,
    'find ready text by locator',
    ['find', 'text', 'Ready marker', 'exists'],
    { found: true },
  );
  await assertCommandData(
    context,
    'find ready text by selector expression',
    ['find', 'text="Ready marker"', 'exists'],
    { found: true },
  );
}

async function assertWebNetwork(context: WebSmokeContext): Promise<void> {
  const result = await runStep(context, 'inspect browser network', [
    'network',
    'dump',
    '10',
    '--include',
    'headers',
    ...context.common,
  ]);
  assert.equal(result.json?.data?.backend, 'agent-browser');
  const entries: unknown[] = Array.isArray(result.json?.data?.entries)
    ? result.json.data.entries
    : [];
  const fixtureEntry = entries.find(
    (entry): entry is Record<string, unknown> =>
      typeof entry === 'object' && entry !== null && 'url' in entry && entry.url === context.url,
  );
  if (!fixtureEntry) {
    failWithContext(context, 'inspect browser network', ['network', 'dump', '10'], result);
  }
  assert.equal(fixtureEntry.method, 'GET');
  assert.equal(typeof fixtureEntry.requestHeaders, 'object');
}

async function assertWebInteractions(context: WebSmokeContext): Promise<void> {
  await runStep(context, 'click submit', ['click', 'label="Submit order"', ...context.common]);
  await runStep(context, 'wait for click result', [
    'wait',
    'text',
    'Submitted',
    '5000',
    ...context.common,
  ]);
  await runStep(context, 'fill email field', [
    'fill',
    'label="Email"',
    'qa@example.test',
    ...context.common,
  ]);
  await runStep(context, 'wait for fill result', [
    'wait',
    'text',
    'Email qa@example.test',
    '5000',
    ...context.common,
  ]);
}

async function assertWebScreenshot(context: WebSmokeContext): Promise<void> {
  await assertCommandData(
    context,
    'capture screenshot artifact',
    ['screenshot', context.screenshotPath, '--full', '--no-stabilize'],
    { path: context.screenshotPath },
  );
  assertPngFile(context.screenshotPath);
}

async function assertCommandData(
  context: WebSmokeContext,
  step: string,
  args: string[],
  expected: Record<string, unknown>,
): Promise<void> {
  const fullArgs = [...args, ...context.common];
  const result = await runStep(context, step, fullArgs);
  for (const [key, value] of Object.entries(expected)) {
    if (result.json?.data?.[key] === value) continue;
    failWithContext(context, step, fullArgs, result, `${key} !== ${JSON.stringify(value)}`);
  }
}

async function runStep(
  context: WebSmokeContext,
  step: string,
  args: string[],
  expectedStatus = 0,
): Promise<CliJsonResult> {
  const result = await runBuiltCliJson(args, context.env);
  recordStep(context, step, args, result);
  maybeCaptureSnapshot(context, args, result);
  if (result.status !== expectedStatus) failWithContext(context, step, args, result);
  return result;
}

async function cleanupWebSmoke(context: WebSmokeContext, opened: boolean): Promise<void> {
  const errors: unknown[] = [];
  if (opened) {
    try {
      await runStep(context, 'close web session', ['close', ...context.common], 0);
    } catch (error) {
      errors.push(error);
    }
  }
  try {
    await closeServer(context.server);
  } catch (error) {
    errors.push(error);
  }
  if (errors.length === 1) {
    throw errors[0];
  }
  if (errors.length > 1) {
    throw new AggregateError(errors, 'web smoke cleanup failed');
  }
}

function recordStep(
  context: WebSmokeContext,
  step: string,
  args: string[],
  result: CliJsonResult,
): void {
  const errorCode =
    typeof result.json?.error?.code === 'string' ? result.json.error.code : undefined;
  const errorMessage =
    typeof result.json?.error?.message === 'string' ? result.json.error.message : undefined;
  context.stepHistory.push({
    step,
    command: `agent-device ${args.join(' ')}`,
    status: result.status,
    timestamp: new Date().toISOString(),
    errorCode,
    errorMessage,
  });
}

function maybeCaptureSnapshot(
  context: WebSmokeContext,
  args: string[],
  result: CliJsonResult,
): void {
  if (args[0] !== 'snapshot' || result.status !== 0) return;
  if (!Array.isArray(result.json?.data?.nodes)) return;
  context.lastSnapshot = result.json;
}

function failWithContext(
  context: WebSmokeContext,
  step: string,
  args: string[],
  result: CliJsonResult,
  assertionDetail?: string,
): never {
  const message = buildFailureDebug(context, step, args, result, assertionDetail);
  writeFailureArtifacts(context, step, args, result, message, assertionDetail);
  assert.fail(message);
}

function buildFailureDebug(
  context: WebSmokeContext,
  step: string,
  args: string[],
  result: CliJsonResult,
  assertionDetail?: string,
): string {
  const lines = [formatResultDebug(step, args, result)];
  if (assertionDetail) lines.push('assertion:', assertionDetail);
  lines.push('last snapshot context:', formatLastSnapshotContext(context));
  lines.push('recent step history:', formatStepHistory(context));
  lines.push('artifacts:', context.artifactDir);
  return lines.join('\n');
}

function writeFailureArtifacts(
  context: WebSmokeContext,
  step: string,
  args: string[],
  result: CliJsonResult,
  message: string,
  assertionDetail?: string,
): void {
  writeFileSync(path.join(context.artifactDir, 'failed-step.txt'), message);
  writeFileSync(
    path.join(context.artifactDir, 'failed-step.json'),
    JSON.stringify(
      { step, command: `agent-device ${args.join(' ')}`, assertionDetail, result },
      null,
      2,
    ),
  );
  writeFileSync(
    path.join(context.artifactDir, 'step-history.json'),
    JSON.stringify(context.stepHistory, null, 2),
  );
  if (context.lastSnapshot) {
    writeFileSync(
      path.join(context.artifactDir, 'last-snapshot.json'),
      JSON.stringify(context.lastSnapshot, null, 2),
    );
  }
}

function formatLastSnapshotContext(context: WebSmokeContext): string {
  const nodes = context.lastSnapshot?.data?.nodes;
  if (!Array.isArray(nodes)) return '(none)';
  const preview = nodes
    .slice(0, 12)
    .map((node: { ref?: unknown; type?: unknown; label?: unknown; rect?: unknown }, i: number) => {
      const rect = node.rect ? JSON.stringify(node.rect) : '(no-bounds)';
      return `${i + 1}. ${String(node.ref ?? '(no-ref)')} type=${String(node.type ?? '(no-type)')} label=${JSON.stringify(node.label ?? '')} rect=${rect}`;
    });
  return [`nodes: ${nodes.length}`, 'nodePreview:', preview.join('\n')].join('\n');
}

function formatStepHistory(context: WebSmokeContext): string {
  return context.stepHistory
    .slice(-8)
    .map((stepRecord) => {
      const error =
        stepRecord.errorCode || stepRecord.errorMessage
          ? ` error=${stepRecord.errorCode ?? ''}${stepRecord.errorMessage ? `:${stepRecord.errorMessage}` : ''}`
          : '';
      return `${stepRecord.timestamp} status=${stepRecord.status}${error} ${stepRecord.step} :: ${stepRecord.command}`;
    })
    .join('\n');
}

async function startFixtureServer(): Promise<{ server: Server; url: string }> {
  const server = createServer((request, response) => {
    if (request.url !== '/') {
      response.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
      response.end('not found');
      return;
    }

    response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
    response.end(fixtureHtml());
  });

  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => {
      server.off('error', reject);
      resolve();
    });
  });

  const address = server.address();
  assert.ok(address && typeof address === 'object', 'fixture server did not bind to a port');
  return { server, url: `http://127.0.0.1:${address.port}/` };
}

async function closeServer(server: Server): Promise<void> {
  if (!server.listening) return;
  await new Promise<void>((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}

function readSnapshotLabels(json: any): string[] {
  const nodes = Array.isArray(json?.data?.nodes) ? json.data.nodes : [];
  return nodes.flatMap((node: { label?: unknown }) =>
    typeof node.label === 'string' && node.label.length > 0 ? [node.label] : [],
  );
}

function createArtifactDir(): string {
  const runId = new Date().toISOString().replaceAll(':', '-');
  const artifactDir = path.resolve('test/artifacts/web/live-web-platform-e2e-smoke', runId);
  mkdirSync(artifactDir, { recursive: true });
  return artifactDir;
}

function fixtureHtml(): string {
  return `<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Agent Device Web Smoke</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        margin: 32px;
      }
      main {
        max-width: 420px;
      }
      label,
      button,
      input {
        display: block;
        font: inherit;
        margin-block: 12px;
      }
      input,
      button {
        min-height: 40px;
        min-width: 220px;
      }
    </style>
  </head>
  <body>
    <main>
      <h1>Agent Device Web Smoke</h1>
      <button id="ready" type="button">Ready marker</button>
      <label for="email">Email</label>
      <input id="email" name="email" aria-label="Email" autocomplete="off" />
      <button id="submit" type="button">Submit order</button>
      <p id="status" role="status" aria-live="polite">Idle</p>
    </main>
    <script>
      const email = document.querySelector('#email');
      const status = document.querySelector('#status');
      const submit = document.querySelector('#submit');
      submit.addEventListener('click', () => {
        submit.textContent = 'Submitted';
        status.textContent = 'Clicked submit';
      });
      email.addEventListener('input', () => {
        email.setAttribute('aria-label', 'Email ' + email.value);
        status.textContent = 'Email: ' + email.value;
      });
    </script>
  </body>
</html>`;
}
