// Sprint H · Subagent Collapsed-Block UI — E2E Tests
// Traces to: sprint-h-subagent-block-spec.md TDD rows 21, 22, 23, 24
//
// ARCHITECTURE NOTE: The spec originally called for a "scenario-provider path" (deterministic
// scripted LLM) gated behind the `test_harness` build tag. That mechanism was removed
// 2026-05-10 — these tests use a real LLM (OPENROUTER_API_KEY_CI required) with temperature=0
// and seed=42 plumbed into OpenRouter requests for maximum determinism (Wave 2.1).
//
// data-testid cross-reference:
//   - [data-testid="subagent-collapsed"]    — SubagentBlock.tsx (collapsed header button)
//   - [data-testid="subagent-expanded"]     — SubagentBlock.tsx (expanded body)
//   - [data-testid="subagent-step-counter"] — SubagentBlock.tsx (step count span)
//   - [data-testid="subagent-live-step"]    — SubagentBlock.tsx (individual step wrapper)
//   - [data-testid="tool-call-badge"]       — ToolCallBadge.tsx

import { expect } from '@playwright/test';
import { test } from './fixtures/console-errors';
import { expectA11yClean } from './fixtures/a11y';
import { chatInput, assistantMessages, newChatButton } from './fixtures/selectors';

// Global storageState provides pre-authenticated session (see playwright.config.ts + global-setup.ts).

// Helper: assert OPENROUTER_API_KEY_CI is present.
// T0.1: no longer soft-skips. The key is required in CI; its absence is a CI
// configuration failure. The function is kept to preserve call-site structure
// but now validates (throws) rather than skipping.
function requireApiKey(_t: typeof test): void {
  if (!process.env.OPENROUTER_API_KEY_CI) {
    throw new Error(
      'BLOCKED: OPENROUTER_API_KEY_CI not set. ' +
      'This test requires a real LLM. ' +
      'The key must be present in CI — see tests/e2e/README.md prerequisites.',
    );
  }
}

// Helper: wait for up to `timeoutMs` ms for a subagent-collapsed block to appear.
// Returns true if found, false if not.
async function waitForSubagentBlock(
  page: import('@playwright/test').Page,
  timeoutMs = 30_000,
): Promise<boolean> {
  try {
    await expect(page.locator('[data-testid="subagent-collapsed"]')).toBeVisible({ timeout: timeoutMs });
    return true;
  } catch {
    return false;
  }
}

// Helper: start a fresh chat session.
async function startFreshChat(page: import('@playwright/test').Page): Promise<void> {
  const newChat = page.getByRole('banner').getByRole('button', { name: 'New Chat' });
  if (await newChat.isVisible({ timeout: 5_000 })) {
    await newChat.click();
    await expect(assistantMessages(page)).toHaveCount(0, { timeout: 10_000 });
  }
}

test.beforeEach(async ({ page }) => {
  await page.goto('/');
});

// ────────────────────────────────────────────────────────────────────────────────
// (a) grandchild refused — Scenario 10, US-3
// BDD: Given a subagent sub-turn is running
//      When the sub-turn's LLM attempts a tool call with name="spawn"
//      Then the tool dispatcher returns an unknown-tool error to the LLM
//      And no subagent_start frame with a grandchild parent_call_id is emitted
//      And the parent's transcript ToolCalls contains exactly one spawn entry
//
// Traces to: sprint-h-subagent-block-spec.md TDD row 21, BDD Scenario 10, lines 304-313
// ────────────────────────────────────────────────────────────────────────────────
test(
  '(a) grandchild refused: subagent attempting spawn gets unknown-tool error, no nested block',
  async ({ page }) => {
    requireApiKey(test);
    // 420s total: this test triggers TWO LLM round-trips (parent spawn +
    // subagent's failed grandchild-spawn attempt) before the collapsed block
    // settles, so the budget is wider than the sibling subagent tests.
    // test.slow()'s 270s was insufficient in CI under suite load — observed
    // 4×156s failures consistently exceeding the 150s collapsed budget.
    test.setTimeout(420_000);

    await startFreshChat(page);

    const input = chatInput(page);
    await expect(input).toBeVisible({ timeout: 15_000 });

    // Deterministic prompt with temperature=0+seed=42 now plumbed into OpenRouter.
    // Commanding, specific: exact tool name, task, and behavior with no optional phrasing.
    await input.fill(
      [
        'Call the `spawn` tool exactly once, right now, with these arguments:',
        '  label: "grandchild test"',
        '  task: "You are the subagent. Your one and only job is to call the `spawn` tool yourself to attempt to spawn a grandchild subagent with task \\"hello\\". If spawn is not in your available tools, report the exact error you receive. Do not do anything else."',
        'Do not reply in prose. Do not call any other tool. Call spawn now.',
      ].join('\n'),
    );
    await input.press('Enter');

    // Structural assertion: wait for at least one subagent-collapsed to appear (the parent spawn).
    // With temperature=0+seed=42 the LLM must comply — if it doesn't, the test fails honestly.
    // 300s budget: this test needs the parent spawn AND the subagent's failed
    // grandchild-spawn round-trip to both complete; under CI load GLM-5v-turbo
    // can take 150-280s for that pair. 150s gave 4×156s timeouts in CI even
    // though local-isolated runs land in 20-40s.
    const collapsedBlocks = page.locator('[data-testid="subagent-collapsed"]');
    await expect(collapsedBlocks.first()).toBeVisible({ timeout: 300_000 });

    const blockCount = await collapsedBlocks.count();

    // Expand the parent block to inspect inner content.
    await collapsedBlocks.first().click();
    const expandedBlock = page.locator('[data-testid="subagent-expanded"]');
    await expect(expandedBlock).toBeVisible({ timeout: 10_000 });

    // Structural assertion: no nested [data-testid="subagent-collapsed"] inside the expanded region.
    // Traces to: BDD Scenario 10 — "no subagent_start frame with a grandchild parent_call_id"
    const nestedCollapsed = expandedBlock.locator('[data-testid="subagent-collapsed"]');
    const nestedCount = await nestedCollapsed.count();
    expect(nestedCount).toBe(0, 'expanded SubagentBlock must contain zero nested subagent-collapsed elements (grandchildren are forbidden — FR-H-006)');

    // Structural assertion: exactly one parent-level collapsed block.
    expect(blockCount).toBe(1, 'exactly one SubagentBlock at parent level — grandchild attempt must not create a second block');

    // Structural assertion: expanded block has child elements (steps or error message).
    const children = await expandedBlock.locator('> *').count();
    expect(children).toBeGreaterThan(0, 'expanded block must have content (steps or error message)');
  },
);

// ────────────────────────────────────────────────────────────────────────────────
// (b) sibling spawns — Scenario 13
// BDD: Given the assistant emits two spawn frames with call_ids c1 then c2
//      When the chat renders the message
//      Then two distinct SubagentBlock elements appear, in the order (c1, c2)
//      And each expands independently without affecting the other
//
// Traces to: sprint-h-subagent-block-spec.md TDD row 22, BDD Scenario 13, lines 334-342
// ────────────────────────────────────────────────────────────────────────────────
test(
  '(b) sibling spawns: two back-to-back spawns render as two independent SubagentBlocks',
  async ({ page }) => {
    requireApiKey(test);
    // test.slow() triples the global 90s test timeout to 270s. Subagent
    // spawn + execution can take 30-90s end-to-end under suite load even
    // though the same test passes in 5-15s alone.
    test.slow();

    await startFreshChat(page);

    const input = chatInput(page);
    await expect(input).toBeVisible({ timeout: 15_000 });

    // Deterministic prompt: explicit, numbered, no prose.
    await input.fill(
      [
        'Call the `spawn` tool exactly TWO times, in sequence. No other tools. No prose answer until both spawns have been issued.',
        '',
        'First call (do this first):',
        '  spawn(label="task one", task="Reply with the word done-one. Use no tools.")',
        '',
        'Second call (do this immediately after the first returns):',
        '  spawn(label="task two", task="Reply with the word done-two. Use no tools.")',
        '',
        'Issue both spawn tool calls now.',
      ].join('\n'),
    );
    await input.press('Enter');

    // Structural assertion: wait for the first collapsed block.
    const collapsedBlocks = page.locator('[data-testid="subagent-collapsed"]');
    await expect(collapsedBlocks.first()).toBeVisible({ timeout: 60_000 });

    // Structural assertion: wait for exactly 2 sibling blocks.
    // Traces to: BDD Scenario 13 — "two distinct SubagentBlock elements"
    await expect(collapsedBlocks).toHaveCount(2, { timeout: 60_000 });

    // Verify independent expansion: expand first — second should remain collapsed.
    await collapsedBlocks.nth(0).click();
    const expandedBlocks = page.locator('[data-testid="subagent-expanded"]');
    await expect(expandedBlocks).toHaveCount(1, { timeout: 10_000 });

    // Expand second — both should now be expanded independently.
    await collapsedBlocks.nth(1).click();
    await expect(expandedBlocks).toHaveCount(2, { timeout: 10_000 });

    // Collapse first — second should remain expanded.
    await collapsedBlocks.nth(0).click();
    await expect(expandedBlocks).toHaveCount(1, { timeout: 10_000 });

    // Differentiation test: two different blocks expanded/collapsed independently.
    const finalCount = await collapsedBlocks.count();
    expect(finalCount).toBe(2, 'exactly 2 sibling SubagentBlocks must be rendered for two spawn calls');
  },
);

// ────────────────────────────────────────────────────────────────────────────────
// (c) live step counter — US-4, Scenario 2
// BDD: Given a sub-turn that fires ≥3 tool_call_start frames with matching parent_call_id
//      When the run progresses
//      Then the collapsed header's step count text increments visibly (0→1→...→≥3)
//
// Traces to: sprint-h-subagent-block-spec.md TDD row 23, BDD Scenario 2, lines 221-229
// ────────────────────────────────────────────────────────────────────────────────
test(
  '(c) live step counter: collapsed header step count increments during multi-step sub-turn',
  async ({ page }) => {
    requireApiKey(test);
    // test.slow() triples the global 90s test timeout to 270s. Subagent
    // spawn + execution can take 30-90s end-to-end under suite load even
    // though the same test passes in 5-15s alone.
    test.slow();

    await startFreshChat(page);

    const input = chatInput(page);
    await expect(input).toBeVisible({ timeout: 15_000 });

    // Deterministic prompt: force a single spawn with a subagent task that mandates ≥3 tool calls.
    // read_file is always registered and does not require special permissions.
    await input.fill(
      [
        'Call the `spawn` tool exactly once, now, with these arguments:',
        '  label: "multi step counter test"',
        '  task: "You are a subagent. You MUST call the read_file tool exactly THREE times in this exact order. Do not skip any call. Do not reply in prose between them. (1) read_file with path=\\"/etc/hostname\\"; (2) read_file with path=\\"/etc/os-release\\"; (3) read_file with path=\\"/proc/version\\". After all three read_file calls have completed, reply with the single word \\"finished\\"."',
        'Do not call any other tool. Do not reply in prose. Call spawn now.',
      ].join('\n'),
    );
    await input.press('Enter');

    // Structural assertion: wait for the collapsed block to appear.
    const collapsedBlock = page.locator('[data-testid="subagent-collapsed"]').first();
    await expect(collapsedBlock).toBeVisible({ timeout: 60_000 });

    // Structural assertion: [data-testid="subagent-step-counter"] must be present.
    // This verifies the step counter element exists in the DOM (FR-H-010).
    const stepCounter = collapsedBlock.locator('[data-testid="subagent-step-counter"]');
    await expect(stepCounter).toBeVisible({ timeout: 5_000 });

    // Poll for ≥3 steps in the step counter text.
    // Traces to: sprint-h-subagent-block-spec.md BDD Scenario 2 — "step counter shows N steps"
    let reachedThreeSteps = false;
    const deadline = Date.now() + 60_000; // 60s budget for multi-step run

    while (Date.now() < deadline) {
      // Scoped catch — only swallow stale-locator errors, rethrow others.
      const counterText = await stepCounter.textContent().catch((err: unknown) => {
        if (err instanceof Error && (err.message.includes('Element is not attached') || err.message.includes('locator handle is stale'))) return null;
        throw err;
      });
      if (!counterText) break;

      const stepMatch = counterText.match(/(\d+)\s+steps?/);
      if (stepMatch) {
        const count = parseInt(stepMatch[1], 10);
        if (count >= 3) {
          reachedThreeSteps = true;
          break;
        }
      }

      // Check if the sub-turn has finished.
      const headerText = await collapsedBlock.textContent().catch((err: unknown) => {
        if (err instanceof Error && (err.message.includes('Element is not attached') || err.message.includes('locator handle is stale'))) return null;
        throw err;
      });
      if (!headerText) break;
      const isFinished = !headerText.includes('working') && !headerText.includes('Running');
      if (isFinished && !reachedThreeSteps) break;

      await page.waitForTimeout(500);
    }

    // Hard assertion: the step counter must have reached ≥3 steps.
    // With temperature=0 the subagent must execute all three read_file calls.
    // If reachedThreeSteps is false, the product did not produce the required steps.
    if (!reachedThreeSteps) {
      // Verify at least the step counter IS rendering (not a missing testid regression).
      const finalCounterText = await stepCounter.textContent().catch(() => '');
      const anySteps = /\d+\s+steps?/.test(finalCounterText ?? '');
      if (!anySteps) {
        throw new Error(
          'PRODUCT REGRESSION: SubagentBlock appeared but [data-testid="subagent-step-counter"] rendered no step count text. ' +
          'Expected text matching /\\d+ steps?/. ' +
          'Traces to: temporal-puzzling-melody.md W2-7, sprint-h-subagent-block-spec.md FR-H-010.',
        );
      }
      throw new Error(
        'ASSERTION FAILED: LLM subagent executed fewer than 3 tool calls. ' +
        'The subagent must follow the prompt and execute 3 read_file calls. ' +
        `Step counter text at timeout: "${finalCounterText}". ` +
        'Traces to: sprint-h-subagent-block-spec.md BDD Scenario 2.',
      );
    }

    expect(reachedThreeSteps).toBe(true);
  },
);

// ────────────────────────────────────────────────────────────────────────────────
// (d) real-LLM smoke — US-1 (best-effort, does NOT gate merge)
// BDD: Uses OpenRouter CI (OPENROUTER_API_KEY_CI env). Triggers a spawn via natural language.
//      Best-effort: asserts only that no JS console errors fire, and that IF a SubagentBlock
//      appears, it behaves correctly. MUST pass or skip gracefully if LLM doesn't call spawn.
//
// Traces to: sprint-h-subagent-block-spec.md TDD row 24, SC-H-003, US-1
// ────────────────────────────────────────────────────────────────────────────────
test(
  '(d) real-LLM smoke: spawn triggered by natural language; no console errors; SubagentBlock if spawned',
  async ({ page, consoleErrors }) => {
    // T0.1: OPENROUTER_API_KEY_CI soft-skip removed. The key is required in CI.
    // This test is best-effort (does not gate merge) but must not skip silently.
    requireApiKey(test);
    // test.slow() triples the global 90s test timeout to 270s; same rationale
    // as the sibling subagent tests (real-LLM spawn under suite load).
    test.slow();

    await startFreshChat(page);

    const input = chatInput(page);
    await expect(input).toBeVisible({ timeout: 15_000 });

    // Natural language prompt — no explicit instruction to use spawn.
    // Let the agent decide whether to spawn based on its own judgment.
    await input.fill(
      'Please have one of your subagents check what files are in the /tmp directory.',
    );
    await input.press('Enter');

    // Wait for assistant to respond (with or without spawn).
    // 90s budget: natural-language prompt (no temperature pinning) is non-deterministic;
    // the assistant may take 30-75s to land its first message under suite load.
    await expect(assistantMessages(page)).toHaveCount(1, { timeout: 90_000 });

    // Best-effort: IF a SubagentBlock appeared, verify basic UI behavior.
    // Use .first(): a natural-language prompt is non-deterministic and glm-5v-turbo
    // sometimes spawns more than one subagent, which would make a bare locator
    // strict-mode-fail on isVisible(). We only care whether >=1 block appeared.
    const collapsedBlock = page.locator('[data-testid="subagent-collapsed"]').first();
    // Scoped catch — only swallow stale-locator errors, rethrow others.
    const spawnOccurred = await collapsedBlock.isVisible({ timeout: 5_000 }).catch((err: unknown) => {
      if (err instanceof Error && (err.message.includes('Element is not attached') || err.message.includes('locator handle is stale'))) return false;
      throw err;
    });

    if (spawnOccurred) {
      // Click to expand — basic expansion must work.
      await collapsedBlock.first().click();
      const expandedBlock = page.locator('[data-testid="subagent-expanded"]');
      await expect(expandedBlock).toBeVisible({ timeout: 10_000 });

      // a11y check on subagent elements (BDD Scenario 11, US-5).
      // Traces to: sprint-h-subagent-block-spec.md Scenario 11, line 316
      await expectA11yClean(page, {
        include: ['[data-testid^="subagent-"]'],
      });
    } else {
      // LLM chose not to spawn — that is acceptable for this smoke test.
      // The primary assertion (no console errors) is captured by the consoleErrors fixture.
      console.info('(d) smoke: LLM did not call spawn — no SubagentBlock rendered. ' +
        'Test passes because no console errors occurred and the response completed.');
    }

    // Primary assertion: zero unexpected JS console errors (captured by consoleErrors fixture).
    // The fixture asserts this automatically at test end via the `consoleErrors` fixture.
    // We force-reference the binding to ensure the fixture is active.
    void consoleErrors;
  },
);

// ────────────────────────────────────────────────────────────────────────────────
// Axe integration: WCAG 2.1 AA against SubagentBlock elements
// Tests both collapsed and expanded states to satisfy US-5 / BDD Scenario 11.
// Traces to: sprint-h-subagent-block-spec.md TDD row 17 (component) + SC-H-006 (E2E layer)
// ────────────────────────────────────────────────────────────────────────────────
test(
  '(e) axe baseline: SubagentBlock elements are WCAG 2.1 AA clean',
  async ({ page }) => {
    requireApiKey(test);
    // test.slow() triples the global 90s test timeout to 270s. Subagent
    // spawn + execution can take 30-90s end-to-end under suite load even
    // though the same test passes in 5-15s alone.
    test.slow();

    await startFreshChat(page);

    const input = chatInput(page);
    await expect(input).toBeVisible({ timeout: 15_000 });

    // The prompt gives the subagent a real reason to exist (running a shell
    // command in isolation) so the LLM doesn't shortcut and answer directly.
    // The previous prompt asked the subagent to "reply ok with no tools" —
    // a smarter LLM correctly skipped spawn because the task was trivial.
    await input.fill(
      [
        'Use the `spawn` tool right now to delegate work to a subagent.',
        'Set label to "axe test subagent".',
        'Set task to: "Use the exec tool to run `echo hello-from-subagent` and return the exact stdout."',
        'Do not run exec yourself — delegate by calling spawn now.',
      ].join('\n'),
    );
    await input.press('Enter');

    // Structural assertion: wait for a SubagentBlock to appear.
    // With temperature=0+seed=42 the LLM must comply — test fails honestly if it doesn't.
    const collapsedBlock = page.locator('[data-testid="subagent-collapsed"]');
    await expect(collapsedBlock.first()).toBeVisible({ timeout: 60_000 });

    // Test 1: axe against collapsed state.
    // Traces to: sprint-h-subagent-block-spec.md Scenario 11 — "collapsed SubagentBlock"
    await expectA11yClean(page, {
      include: ['[data-testid^="subagent-"]'],
    });

    // Test 2: expand the block and run axe again against expanded state.
    // Traces to: sprint-h-subagent-block-spec.md Scenario 11 — "expanded SubagentBlock"
    await collapsedBlock.first().click();
    const expandedBlock = page.locator('[data-testid="subagent-expanded"]');
    await expect(expandedBlock).toBeVisible({ timeout: 10_000 });

    await expectA11yClean(page, {
      include: ['[data-testid^="subagent-"]'],
    });
  },
);
