import { test, expect } from '@playwright/test';
import { createRequire } from 'node:module';
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

// ── Public app-share view E2E (login-free, read-only) ─────────────────────────
//
// REQUIRES `npm run test:e2e` + a running server (ui/playwright.config.ts boots
// the real orchestrator with auth OFF → synthetic 'local' user). This does NOT
// run in the sandbox — there is no live server here.
//
// A workspace app (apps/{name}/index.html) inside a space can be shared via a
// login-free, read-only public URL: /ui/app/:token. The public viewer
// (SharedAppView → AppRunner) resolves the token through /api/app-share/:token
// (no auth), renders the entry HTML in a sandboxed iframe, and exposes a
// read-only gateway (no write/delete). The published-tested logic libs
// (appShareUrl / sharedTabs / sharedView) are unit-covered; this proves the
// end-to-end public viewer flow that has no e2e.
//
// We cannot create an app + share link through the LLM-less UI, so we seed engine
// state directly into the throwaway DB + workspace dir (same handoff the
// tool-request spec uses): the DB path is written by playwright.config.ts to
// ui/.e2e-db-path, and the workspace dir is its sibling 'workspaces' folder
// (E2E_TMP/{e2e.db, workspaces}). We create a space via the built Repository,
// drop an entry HTML under {worktree}/space/{id}/files/apps/{app}/index.html
// (matching spaceFilesDir + findEntryPath), and mint a share token. Then the
// browser opens the public URL with NO session.

const __dirname = dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
const repoRoot = resolve(__dirname, '..', '..');

// playwright.config.ts hands off DB_PATH = {E2E_TMP}/e2e.db and
// WORKTREE_DIR = {E2E_TMP}/workspaces (siblings). Derive the worktree dir from
// the DB path so both agree on the SAME temp tree the webServer booted with.
const dbPath = readFileSync(resolve(__dirname, '..', '.e2e-db-path'), 'utf-8').trim();
const e2eTmp = dirname(dbPath);
const WORKTREE_DIR = join(e2eTmp, 'workspaces');

const APP_NAME = 'e2e-public-app';
// A self-contained entry page (no relative assets) so the iframe render is
// deterministic without extra file fetches.
const APP_MARKER = 'E2E PUBLIC APP CONTENT';
const APP_HTML = `<!doctype html><html><head><meta charset="utf-8"><title>${APP_NAME}</title></head>` +
  `<body><h1>${APP_MARKER}</h1></body></html>`;

let spaceId = '';
let token = '';

test.beforeAll(async () => {
  const { Repository } = require(resolve(repoRoot, 'dist/db/repository.js')) as {
    Repository: new (dbPath: string) => {
      createSpace: (p: { kind: string; title: string; ownerId: string; visibility?: string }) => Promise<{ id: string }>;
      createAppShareLink: (spaceId: string, appName: string, createdBy: string | null) => { token: string };
      close?: () => void;
    };
  };

  const repo = new Repository(dbPath);
  try {
    // Under no-auth the synthetic owner is 'local'.
    const space = await repo.createSpace({ kind: 'case', title: '案件-公開アプリ共有', ownerId: 'local', visibility: 'private' });
    spaceId = space.id;

    // Entry HTML at the location findEntryPath() prefers:
    // {worktree}/space/{id}/files/apps/{app}/index.html
    const appDir = join(WORKTREE_DIR, 'space', spaceId, 'files', 'apps', APP_NAME);
    mkdirSync(appDir, { recursive: true });
    writeFileSync(join(appDir, 'index.html'), APP_HTML, 'utf-8');

    // Mint the public share token (createdBy null under no-auth).
    token = repo.createAppShareLink(spaceId, APP_NAME, null).token;
  } finally {
    repo.close?.();
  }
});

// Happy path: opening the public URL with NO login resolves the token, renders
// the AppRunner full-screen, marks it read-only, and the seeded entry HTML loads
// into the sandboxed iframe.
test('public app-share URL renders the read-only viewer without login', async ({ page }) => {
  expect(token, 'share token seeded').toBeTruthy();

  // No session is established — this is the login-free public route.
  await page.goto(`/ui/app/${token}`);

  // The AppRunner shell renders (the public viewer mounts it full-screen).
  const runner = page.getByTestId('app-runner');
  await expect(runner).toBeVisible({ timeout: 15_000 });

  // The read-only badge is shown (writable=false → the public-share copy). The
  // text is a hardcoded literal, not i18n, so it is locale-independent.
  await expect(runner).toContainText('公開共有（read-only）');

  // The entry HTML is loaded into the sandboxed iframe (srcDoc), so the frame
  // element is present.
  await expect(page.getByTestId('app-runner-frame')).toBeVisible();
  const frame = page.frameLocator('[data-testid="app-runner-frame"]');
  await expect(frame.getByText(APP_MARKER)).toBeVisible({ timeout: 15_000 });

  // The public meta API is reachable WITHOUT auth and resolves the app.
  const meta = await page.request.get(`/api/app-share/${token}`);
  expect(meta.status(), 'public meta resolves').toBe(200);
  const body = (await meta.json()) as { app?: { appName?: string; entryPath?: string | null } };
  expect(body.app?.appName).toBe(APP_NAME);
  expect(body.app?.entryPath).toContain(`apps/${APP_NAME}/`);
});

// Negative/visibility: an invalid/unknown token yields the public 404 screen
// (and the public meta API 404s) — never the real app.
test('an invalid app-share token shows the public not-found screen', async ({ page }) => {
  await page.goto('/ui/app/this-token-does-not-exist');

  // The 404 tone screen renders; the AppRunner must NOT mount.
  await expect(page.getByText('アプリが見つかりません')).toBeVisible({ timeout: 15_000 });
  await expect(page.getByTestId('app-runner')).toHaveCount(0);

  // The public meta API rejects the unknown token.
  const meta = await page.request.get('/api/app-share/this-token-does-not-exist');
  expect(meta.status()).toBe(404);
});
