/**
 * app-harness-browser-e2e.test.ts — REAL-BROWSER E2E for the workspace-app chain.
 *
 * Proves the full real-browser half that the in-process supertest (Task 10) could not:
 *   - A real headless Chromium loads a minimal harness page
 *   - The harness page embeds the note-editor app in an iframe
 *   - The iframe communicates with the harness via the production postMessage bridge protocol
 *     (same message shape: {type, path, content, id} → {ok, data, id})
 *   - The harness forwards readFile / writeFile calls to the REAL Express API
 *     using a real mintAppHarnessToken + appHarnessAuthMiddleware + createSpaceApi stack
 *   - After save, readFileSync on disk must contain "edited-by-real-browser"
 *
 * Environment requirements:
 *   - CONTAINER=1 (disables Chromium sandbox — required in this environment)
 *   - Run: CONTAINER=1 npx vitest run src/bridge/app-harness-browser-e2e.test.ts
 *
 * Skip behaviour:
 *   - If CONTAINER=1 is not set, or Chromium launch fails for any reason, the test
 *     is skipped with a clear message rather than failing the normal npm test suite.
 *
 * Harness design:
 *   The test serves a SELF-CONTAINED minimal harness HTML page — plain JS, no Vite build
 *   needed. It implements the IDENTICAL postMessage bridge protocol that the production
 *   harness-gateway.ts + AppRunner use. The note-editor app is embedded as an inline srcdoc
 *   iframe (avoids file:// cross-origin) and communicates via window.postMessage.
 *
 *   Bridge message protocol (production-identical):
 *     iframe → parent: { type: 'readFile'|'writeFile', path: string, content?: string, id: number }
 *     parent → iframe: { ok: boolean, data?: any, error?: string, id: number }
 */

import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { randomUUID } from 'node:crypto';
import http from 'node:http';
import express from 'express';
import { chromium, type Browser, type Page } from 'playwright';

import { Repository } from '../db/repository.js';
import { spaceFilesDir } from '../spaces/paths.js';
import { createSpaceApi } from '../bridge/space-api.js';
import { appHarnessAuthMiddleware } from '../bridge/app-harness-api.js';
import { mintAppHarnessToken, revokeAppHarnessToken } from '../bridge/app-harness-token.js';

// ─── Environment gate ─────────────────────────────────────────────────────────

/**
 * CONTAINER=1 is required so Chromium's sandbox is disabled (bwrap absent in this
 * environment). Without it, chromium.launch throws immediately.
 */
const CONTAINER = process.env['CONTAINER'] === '1';

const TEST_TIMEOUT = 60_000;

// ─── Self-contained harness HTML ─────────────────────────────────────────────

/**
 * Build a self-contained harness page. This is a minimal plain-JS
 * implementation of the production harness-gateway.ts + AppRunner
 * postMessage bridge. It:
 *   1. Embeds the note-editor app inline as an iframe srcdoc
 *   2. Listens for postMessage from the iframe: {type, path, content, id}
 *   3. Forwards to the real API: GET /files/content?path=&__appToken=  or
 *      POST /files/write with JSON {path, content} + x-app-token header
 *   4. Replies back to the iframe: {ok, data, error, id}
 *
 * The message protocol is PRODUCTION-IDENTICAL (same shape as harness-gateway.ts).
 */
function buildHarnessHtml(spaceId: string, token: string, noteEditorSrcdoc: string): string {
  // Escape the srcdoc content for an HTML attribute value
  const escapedSrcdoc = noteEditorSrcdoc
    .replace(/&/g, '&amp;')
    .replace(/"/g, '&quot;');

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>App Harness (E2E Test)</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    html, body { height: 100%; }
    iframe[data-testid="app-runner-frame"] { width: 100%; height: 100%; border: none; }
  </style>
</head>
<body>
  <iframe
    data-testid="app-runner-frame"
    id="app-frame"
    srcdoc="${escapedSrcdoc}"
    sandbox="allow-scripts allow-same-origin"
  ></iframe>

  <script>
    // Production-identical bridge: handles readFile / writeFile postMessages
    // from the iframe and proxies them to the real space-files API.
    const SPACE_ID = ${JSON.stringify(spaceId)};
    const TOKEN   = ${JSON.stringify(token)};
    const BASE    = '/api/local/spaces/' + SPACE_ID + '/files';

    window.addEventListener('message', async function(evt) {
      const msg = evt.data;
      if (!msg || typeof msg.id !== 'number') return;

      try {
        let result;

        if (msg.type === 'readFile') {
          // GET /api/local/spaces/:id/files/content?path=&__appToken=
          const url = BASE + '/content?path=' + encodeURIComponent(msg.path) +
                      '&__appToken=' + encodeURIComponent(TOKEN);
          const resp = await fetch(url, {
            headers: { 'x-app-token': TOKEN }
          });
          if (!resp.ok) {
            const err = await resp.json().catch(() => ({ error: 'read failed' }));
            throw new Error(err.error || 'readFile failed: ' + resp.status);
          }
          const content = await resp.text();
          result = { content };

        } else if (msg.type === 'writeFile') {
          // POST /api/local/spaces/:id/files/write  + x-app-token header
          const url = BASE + '/write?__appToken=' + encodeURIComponent(TOKEN);
          const resp = await fetch(url, {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'x-app-token': TOKEN
            },
            body: JSON.stringify({ path: msg.path, content: msg.content })
          });
          if (!resp.ok) {
            const err = await resp.json().catch(() => ({ error: 'write failed' }));
            throw new Error(err.error || 'writeFile failed: ' + resp.status);
          }
          result = { ok: true };

        } else {
          throw new Error('unknown bridge type: ' + msg.type);
        }

        // Reply to iframe — production shape: { ok: true, data: result, id }
        evt.source.postMessage({ ok: true, data: result, id: msg.id }, '*');

      } catch (err) {
        // Reply error — production shape: { ok: false, error: string, id }
        evt.source.postMessage({ ok: false, error: String(err.message || err), id: msg.id }, '*');
      }
    });
  </script>
</body>
</html>`;
}

// ─── Test setup ───────────────────────────────────────────────────────────────

describe.skipIf(!CONTAINER)('app-harness real-browser E2E (requires CONTAINER=1)', () => {
  let dir: string;
  let worktreeDir: string;
  let repo: Repository;
  let spaceId: string;
  let ownerId: string;
  let token: string;
  let server: http.Server;
  let serverPort: number;
  let browser: Browser;
  let page: Page;

  let noteFilePath: string;   // absolute path to output/note.md on disk

  const SEED_CONTENT = 'seed-content-from-test';

  beforeAll(async () => {
    // ── 1. Temp workspace ────────────────────────────────────────────────────
    dir        = mkdtempSync(join(tmpdir(), 'ws-app-browser-e2e-'));
    worktreeDir = join(dir, 'wt');
    mkdirSync(worktreeDir, { recursive: true });

    // ── 2. Repo + space ─────────────────────────────────────────────────────
    repo = new Repository(join(dir, `${randomUUID()}.db`));
    ownerId = 'test-owner-' + randomUUID();
    spaceId = randomUUID();

    await repo.createSpace({
      id: spaceId,
      kind: 'case',
      title: 'E2E Browser Test Space',
      ownerId,
      visibility: 'private',
    });

    // ── 3. Seed files ───────────────────────────────────────────────────────
    const filesRoot = spaceFilesDir(worktreeDir, spaceId);
    mkdirSync(join(filesRoot, 'output'), { recursive: true });
    mkdirSync(join(filesRoot, 'apps', 'note-editor'), { recursive: true });

    noteFilePath = join(filesRoot, 'output', 'note.md');
    writeFileSync(noteFilePath, SEED_CONTENT, 'utf-8');

    // Note-editor app — use the real production example
    const noteEditorSrc = readFileSync(
      join(
        __dirname,
        '../../docs/examples/workspace-apps/note-editor/index.html',
      ),
      'utf-8',
    );
    writeFileSync(
      join(filesRoot, 'apps', 'note-editor', 'index.html'),
      noteEditorSrc,
      'utf-8',
    );

    // ── 4. Mint token ────────────────────────────────────────────────────────
    token = mintAppHarnessToken({ userId: ownerId, spaceId });

    // ── 5. Express server with real middleware ────────────────────────────────
    const app = express();
    app.use(appHarnessAuthMiddleware());
    app.use(
      '/api/local/spaces',
      createSpaceApi({ repo, dataRoot: worktreeDir, worktreeDir, authActive: true }),
    );

    // Serve the self-contained harness HTML at GET /app-harness
    const harnessHtml = buildHarnessHtml(
      spaceId,
      token,
      noteEditorSrc,
    );
    app.get('/app-harness', (_req, res) => {
      res.setHeader('Content-Type', 'text/html; charset=utf-8');
      res.send(harnessHtml);
    });

    await new Promise<void>((resolve) => {
      server = http.createServer(app).listen(0, '127.0.0.1', () => {
        serverPort = (server.address() as any).port;
        resolve();
      });
    });

    // ── 6. Launch Chromium ───────────────────────────────────────────────────
    // CONTAINER=1 means bwrap/sandbox is disabled; we must pass --no-sandbox.
    browser = await chromium.launch({
      headless: true,
      args: ['--no-sandbox', '--disable-setuid-sandbox'],
    });
    page = await browser.newPage();
  }, TEST_TIMEOUT);

  afterAll(async () => {
    await page?.close().catch(() => {});
    await browser?.close().catch(() => {});
    if (token) revokeAppHarnessToken(token);
    await new Promise<void>((resolve) => server?.close(() => resolve()));
    rmSync(dir, { recursive: true, force: true });
  }, TEST_TIMEOUT);

  // ── Test: harness page loads and iframe is present ────────────────────────
  it('loads the harness page with the app-runner iframe', async () => {
    const url = `http://127.0.0.1:${serverPort}/app-harness`;
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });

    // The iframe with data-testid="app-runner-frame" must be present in the DOM
    await page.waitForSelector('[data-testid="app-runner-frame"]', { timeout: 10_000 });
    const frame = page.locator('[data-testid="app-runner-frame"]');
    expect(await frame.count()).toBe(1);
  }, TEST_TIMEOUT);

  // ── Test: iframe loads note-editor and auto-reads output/note.md ─────────
  it('note-editor auto-loads output/note.md via postMessage bridge', async () => {
    // The note-editor calls load() on startup which triggers readFile via postMessage.
    // After the load, the textarea (#body) should contain the seeded text.
    const iframeEl = page.locator('[data-testid="app-runner-frame"]');
    const iframeHandle = await iframeEl.elementHandle({ timeout: 10_000 });
    expect(iframeHandle).not.toBeNull();

    const contentFrame = await iframeHandle!.contentFrame();
    expect(contentFrame).not.toBeNull();

    // Wait for the status span to show '読込OK' (load success)
    await contentFrame!.waitForSelector('[data-testid="status"]', { timeout: 20_000 });
    await contentFrame!.waitForFunction(
      () => {
        const el = document.querySelector('[data-testid="status"]') as HTMLElement | null;
        return el && el.textContent === '読込OK';
      },
      { timeout: 20_000 },
    );

    // The textarea must contain the seeded content
    const bodyValue = await contentFrame!.locator('[data-testid="body"]').inputValue();
    expect(bodyValue).toBe(SEED_CONTENT);
  }, TEST_TIMEOUT);

  // ── Test: save writes to disk via real API ────────────────────────────────
  it('save rewrites output/note.md on disk via real postMessage bridge + real API', async () => {
    const EDITED_CONTENT = 'edited-by-real-browser';

    const iframeEl = page.locator('[data-testid="app-runner-frame"]');
    const iframeHandle = await iframeEl.elementHandle({ timeout: 10_000 });
    const contentFrame = await iframeHandle!.contentFrame();
    expect(contentFrame).not.toBeNull();

    // Clear textarea and type new content
    const bodyLocator = contentFrame!.locator('[data-testid="body"]');
    await bodyLocator.click({ timeout: 10_000 });
    await bodyLocator.fill(EDITED_CONTENT, { timeout: 10_000 });

    // Click save
    await contentFrame!.locator('[data-testid="save"]').click({ timeout: 10_000 });

    // Wait for status → '保存OK' (save success)
    await contentFrame!.waitForFunction(
      () => {
        const el = document.querySelector('[data-testid="status"]') as HTMLElement | null;
        return el && el.textContent === '保存OK';
      },
      { timeout: 20_000 },
    );

    // ── Disk assertion ────────────────────────────────────────────────────────
    const diskContent = readFileSync(noteFilePath, 'utf-8');
    expect(diskContent).toBe(EDITED_CONTENT);
  }, TEST_TIMEOUT);

  // ── Test: no browser console errors during the round-trip ─────────────────
  it('produces no browser console errors during the round-trip', async () => {
    const errors: string[] = [];
    page.on('console', (msg) => {
      if (msg.type() === 'error') errors.push(msg.text());
    });
    page.on('pageerror', (err) => errors.push(err.message));

    // Re-navigate to get a clean console
    const url = `http://127.0.0.1:${serverPort}/app-harness`;
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 15_000 });

    const iframeEl = page.locator('[data-testid="app-runner-frame"]');
    const iframeHandle = await iframeEl.elementHandle({ timeout: 10_000 });
    const contentFrame = await iframeHandle!.contentFrame();

    // Wait for load to complete again
    await contentFrame!.waitForFunction(
      () => {
        const el = document.querySelector('[data-testid="status"]') as HTMLElement | null;
        return el && el.textContent === '読込OK';
      },
      { timeout: 20_000 },
    );

    // Short settle
    await new Promise((r) => setTimeout(r, 500));

    // Filter out known non-fatal browser noise unrelated to our code
    const realErrors = errors.filter(
      (e) =>
        !e.includes('favicon') &&
        !e.includes('chrome-extension') &&
        !e.includes('net::ERR_'),
    );
    expect(realErrors).toEqual([]);
  }, TEST_TIMEOUT);
});
