// @vitest-environment jsdom

import { act } from 'react';
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { HomeView } from '../../src/components/HomeView';
import {
  createPluginAuthoringHandoff,
  createPluginUseHandoff,
  PLUGIN_AUTHORING_DEFAULT_GOAL,
  PLUGIN_AUTHORING_PROMPT,
} from '../../src/components/home-hero/plugin-authoring';
// HomeHero's `home-hero-input` is now the project composer's Lexical
// contenteditable, not a <textarea>. These helpers drive/read it through the
// live editor instead of synthetic `fireEvent.change` / `.value` (which are
// no-ops on a contenteditable).
import {
  homeHeroPromptText,
  setHomeHeroPrompt,
} from '../helpers/home-hero-lexical';

const AUTHORING_PLUGIN = {
  id: 'od-plugin-authoring',
  title: 'Plugin authoring',
  version: '0.1.0',
  trust: 'bundled' as const,
  sourceKind: 'bundled' as const,
  source: '/tmp/plugin-authoring',
  capabilitiesGranted: ['prompt:inject'],
  fsPath: '/tmp/plugin-authoring',
  installedAt: 0,
  updatedAt: 0,
  manifest: {
    name: 'od-plugin-authoring',
    title: 'Plugin authoring',
    version: '0.1.0',
    description: 'Create plugins',
    od: {
      kind: 'scenario',
      taskKind: 'new-generation',
      useCase: { query: 'Create an Open Design plugin for {{pluginGoal}}.' },
      inputs: [
        {
          name: 'pluginGoal',
          type: 'string',
          required: false,
          default: PLUGIN_AUTHORING_DEFAULT_GOAL,
          label: 'Plugin goal',
        },
      ],
    },
  },
};

const DEFAULT_PLUGIN = {
  ...AUTHORING_PLUGIN,
  id: 'od-new-generation',
  title: 'New generation',
  source: '/tmp/new-generation',
  fsPath: '/tmp/new-generation',
  manifest: {
    ...AUTHORING_PLUGIN.manifest,
    name: 'od-new-generation',
    title: 'New generation',
    description: 'Create new design artifacts',
    od: {
      kind: 'scenario',
      taskKind: 'new-generation',
      useCase: { query: 'Create a plugin.' },
    },
  },
};

const HIDDEN_DEFAULT_PLUGIN = {
  ...DEFAULT_PLUGIN,
  id: 'od-default',
  title: 'Default design router',
  source: '/tmp/default-router',
  fsPath: '/tmp/default-router',
  manifest: {
    ...DEFAULT_PLUGIN.manifest,
    name: 'od-default',
    title: 'Default design router',
    od: {
      ...DEFAULT_PLUGIN.manifest.od,
      hidden: true,
    },
  },
};

// The Prototype chip binds to the bundled `example-web-prototype`
// plugin (which ships its own seed + layouts + checklist) instead of
// the generic od-new-generation router. Mirror that here so the
// chip-applies test can find a matching plugin record and the apply
// call resolves to the new id.
const WEB_PROTOTYPE_PLUGIN = {
  ...DEFAULT_PLUGIN,
  id: 'example-web-prototype',
  title: 'Web Prototype',
  source: '/tmp/web-prototype',
  fsPath: '/tmp/web-prototype',
  manifest: {
    ...DEFAULT_PLUGIN.manifest,
    name: 'example-web-prototype',
    title: 'Web Prototype',
    description: 'General-purpose desktop web prototype.',
    od: {
      kind: 'scenario',
      taskKind: 'new-generation',
      useCase: {
        query: 'Build a {{fidelity}} {{artifactKind}} for {{audience}} using {{designSystem}} from {{template}}.',
      },
      inputs: [
        {
          name: 'artifactKind',
          type: 'string',
          required: true,
          default: 'web prototype',
          label: 'Artifact kind',
        },
        {
          name: 'fidelity',
          type: 'select',
          required: true,
          options: ['wireframe', 'high-fidelity'],
          default: 'high-fidelity',
          label: 'Fidelity',
        },
        {
          name: 'audience',
          type: 'string',
          required: true,
          default: 'product evaluators',
          label: 'Audience',
        },
        {
          name: 'designSystem',
          type: 'string',
          default: 'the active project design system',
          label: 'Design system',
        },
        {
          name: 'template',
          type: 'string',
          default: 'the bundled web prototype seed',
          label: 'Template',
        },
      ],
    },
  },
};

const SIMPLE_DECK_PLUGIN = {
  ...DEFAULT_PLUGIN,
  id: 'example-simple-deck',
  title: 'Simple Deck',
  source: '/tmp/simple-deck',
  fsPath: '/tmp/simple-deck',
  manifest: {
    ...DEFAULT_PLUGIN.manifest,
    name: 'example-simple-deck',
    title: 'Simple Deck',
    description: 'Single-file horizontal-swipe HTML deck.',
    od: {
      kind: 'scenario',
      taskKind: 'new-generation',
      useCase: {
        query: 'Create a {{deckType}} for {{audience}} about {{topic}} with {{slideCount}}. Speaker notes: {{speakerNotes}}. Use {{designSystem}}.',
      },
      inputs: [
        {
          name: 'deckType',
          type: 'select',
          required: true,
          options: ['pitch deck', 'product overview', 'study deck'],
          default: 'pitch deck',
          label: 'Deck type',
        },
        {
          name: 'topic',
          type: 'string',
          required: true,
          default: 'the user brief',
          label: 'Topic',
        },
        {
          name: 'audience',
          type: 'string',
          required: true,
          default: 'decision makers',
          label: 'Audience',
        },
        {
          name: 'slideCount',
          type: 'select',
          required: true,
          options: ['5-10 pages', '10-15 pages', '15-20 pages', '20-25 pages', '25-30 pages'],
          default: '10-15 pages',
          label: 'Pages',
        },
        {
          name: 'speakerNotes',
          type: 'select',
          options: ['include speaker notes', 'no speaker notes'],
          default: 'include speaker notes',
          label: 'Speaker notes',
        },
        {
          name: 'designSystem',
          type: 'string',
          default: 'the active project design system',
          label: 'Design system',
        },
      ],
    },
  },
};

const LIVE_ARTIFACT_PLUGIN = {
  ...DEFAULT_PLUGIN,
  id: 'example-live-artifact',
  title: 'Live Artifact',
  source: '/tmp/live-artifact',
  fsPath: '/tmp/live-artifact',
  manifest: {
    ...DEFAULT_PLUGIN.manifest,
    name: 'example-live-artifact',
    title: 'Live Artifact',
    description: 'Create refreshable, auditable Open Design artifacts.',
    od: {
      kind: 'scenario',
      taskKind: 'new-generation',
      mode: 'prototype',
      scenario: 'live',
      useCase: {
        query: 'Create refreshable, auditable Open Design artifacts backed by connector or local data.',
      },
      context: {
        skills: [{ path: './SKILL.md' }],
      },
      pipeline: {
        stages: [{ id: 'generate', atoms: ['file-write', 'live-artifact'] }],
      },
    },
  },
};

const LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN = {
  ...LIVE_ARTIFACT_PLUGIN,
  id: 'image-template-notion-team-dashboard-live-artifact',
  title: 'Notion live artifact',
  source: '/tmp/notion-live-artifact',
  fsPath: '/tmp/notion-live-artifact',
  manifest: {
    ...LIVE_ARTIFACT_PLUGIN.manifest,
    name: 'image-template-notion-team-dashboard-live-artifact',
    title: 'Notion live artifact',
    description: 'Create a live Notion dashboard artifact.',
    od: {
      ...LIVE_ARTIFACT_PLUGIN.manifest.od,
      mode: 'image',
      surface: 'image',
      useCase: {
        query: 'Create a refreshable Notion dashboard live artifact.',
      },
    },
  },
};

const AUTHORING_DEFAULT_SCENARIO_INPUTS = {
  artifactKind: 'Open Design plugin',
  audience: 'Open Design plugin authors',
  topic: 'packaging a reusable workflow as an Open Design plugin',
};

const REFLY_DESIGN_SYSTEM = {
  id: 'ds-refly',
  title: 'Refly Design System',
  category: 'Productivity & SaaS',
  summary: 'Refly defaults',
  source: 'user' as const,
  status: 'published' as const,
  isEditable: true,
};

const AUTHORING_APPLY_RESULT = {
  query: 'Create a plugin.',
  contextItems: [],
  inputs: AUTHORING_PLUGIN.manifest.od.inputs,
  assets: [],
  mcpServers: [],
  trust: 'trusted',
  capabilitiesGranted: ['prompt:inject'],
  capabilitiesRequired: ['prompt:inject'],
  appliedPlugin: {
    snapshotId: 'snap-authoring',
    pluginId: 'od-plugin-authoring',
    pluginVersion: '0.1.0',
    manifestSourceDigest: 'a'.repeat(64),
    inputs: { pluginGoal: PLUGIN_AUTHORING_DEFAULT_GOAL },
    resolvedContext: { items: [] },
    capabilitiesGranted: ['prompt:inject'],
    capabilitiesRequired: ['prompt:inject'],
    assetsStaged: [],
    taskKind: 'new-generation',
    appliedAt: 0,
    connectorsRequired: [],
    connectorsResolved: [],
    mcpServers: [],
    status: 'fresh',
  },
  projectMetadata: {},
};

const DEFAULT_APPLY_RESULT = {
  ...AUTHORING_APPLY_RESULT,
  inputs: [],
  appliedPlugin: {
    ...AUTHORING_APPLY_RESULT.appliedPlugin,
    snapshotId: 'snap-default',
    pluginId: 'od-new-generation',
    inputs: AUTHORING_DEFAULT_SCENARIO_INPUTS,
  },
};

const WEB_PROTOTYPE_APPLY_RESULT = {
  ...AUTHORING_APPLY_RESULT,
  query: WEB_PROTOTYPE_PLUGIN.manifest.od.useCase.query,
  inputs: WEB_PROTOTYPE_PLUGIN.manifest.od.inputs,
  appliedPlugin: {
    ...AUTHORING_APPLY_RESULT.appliedPlugin,
    snapshotId: 'snap-web-prototype',
    pluginId: 'example-web-prototype',
    inputs: {
      artifactKind: 'web prototype',
      fidelity: 'high-fidelity',
      audience: 'product evaluators',
      designSystem: 'the active project design system',
      template: 'the bundled web prototype seed',
    },
  },
};

const SIMPLE_DECK_APPLY_RESULT = {
  ...AUTHORING_APPLY_RESULT,
  query: SIMPLE_DECK_PLUGIN.manifest.od.useCase.query,
  inputs: SIMPLE_DECK_PLUGIN.manifest.od.inputs,
  appliedPlugin: {
    ...AUTHORING_APPLY_RESULT.appliedPlugin,
    snapshotId: 'snap-simple-deck',
    pluginId: 'example-simple-deck',
    inputs: {
      deckType: 'pitch deck',
      topic: 'the user brief',
      audience: 'decision makers',
      slideCount: '10-15 pages',
      speakerNotes: 'include speaker notes',
      designSystem: 'the active project design system',
    },
  },
};

const LIVE_ARTIFACT_APPLY_RESULT = {
  ...AUTHORING_APPLY_RESULT,
  query: LIVE_ARTIFACT_PLUGIN.manifest.od.useCase.query,
  inputs: [],
  appliedPlugin: {
    ...AUTHORING_APPLY_RESULT.appliedPlugin,
    snapshotId: 'snap-live-artifact',
    pluginId: 'example-live-artifact',
    inputs: {},
  },
  projectMetadata: {
    skillId: 'live-artifact',
  },
};

function stubAnimationFrame() {
  vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => {
    const id = window.setTimeout(() => cb(window.performance.now()), 0);
    return id;
  });
  vi.stubGlobal('cancelAnimationFrame', (id: number) => {
    window.clearTimeout(id);
  });
}

describe('HomeView prompt handoff', () => {
  afterEach(() => {
    vi.unstubAllGlobals();
    cleanup();
  });

  it('consumes a plugin authoring handoff once and focuses the textarea', async () => {
    let resolveApply: (response: Response) => void = () => undefined;
    const applyResponse = new Promise<Response>((resolve) => {
      resolveApply = resolve;
    });
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [AUTHORING_PLUGIN, WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/od-plugin-authoring/apply')) {
        return applyResponse;
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();

    const { rerender } = render(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
        promptHandoff={createPluginAuthoringHandoff(1)}
      />,
    );

    const input = await screen.findByTestId('home-hero-input');
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(PLUGIN_AUTHORING_PROMPT);
      expect(document.activeElement).toBe(input);
    });
    const inputCard = input.closest('.home-hero__input-card') as HTMLElement | null;
    expect(inputCard?.classList.contains('home-hero__input-card--compact-authoring')).toBe(true);
    expect(inputCard?.style.getPropertyValue('--home-hero-prompt-max-height')).toBe('132px');

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/od-plugin-authoring/apply',
      expect.anything(),
    ));
    resolveApply(new Response(JSON.stringify(AUTHORING_APPLY_RESULT), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    }));
    await waitFor(() => {
      expect((screen.getByTestId('home-hero-submit') as HTMLButtonElement).disabled).toBe(false);
    });

    await setPromptAndSettle('User edited prompt');

    rerender(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
        promptHandoff={createPluginAuthoringHandoff(1)}
      />,
    );

    expect(homeHeroPromptText()).toBe('User edited prompt');
  });

  it('uses the same authoring prompt from the Home rail chip', async () => {
    vi.stubGlobal('fetch', vi.fn(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [AUTHORING_PLUGIN, WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/od-plugin-authoring/apply')) {
        return new Response(JSON.stringify(AUTHORING_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    }));
    stubAnimationFrame();

    render(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    await clickHomeShortcut('create-plugin');

    const input = await screen.findByTestId('home-hero-input');
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(PLUGIN_AUTHORING_PROMPT);
      expect(document.activeElement).toBe(input);
    });
    expect(screen.queryByRole('alert')).toBeNull();
  });

  it('adds a plugin-use handoff from the Plugins page as context', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();

    render(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
        promptHandoff={createPluginUseHandoff(1, 'example-web-prototype')}
      />,
    );

    // The per-plugin context badge row was removed; staged plugin context now
    // only renders as an inline @mention pill (or, when the prompt is empty as
    // here, surfaces via the active context row's resolved-count label). Assert
    // the plugin was staged through that count rather than the dropped badge.
    await waitFor(() => {
      expect(screen.getByLabelText(/1 context items resolved/i)).toBeTruthy();
    });
    await screen.findByTestId('home-hero-input');
    expect(homeHeroPromptValue()).toBe('');
    expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/apply'))).toBe(false);
  });

  it('routes free-form submits through the hidden default plugin without applying a visible chip', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [HIDDEN_DEFAULT_PLUGIN, DEFAULT_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await screen.findByTestId('home-hero-input');
    await setPromptAndSettle('Make a launch page for a robotics studio');
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    expect(screen.queryByTestId('home-hero-active-plugin')).toBeNull();
    expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      prompt: 'Make a launch page for a robotics studio',
      pluginId: 'od-default',
      appliedPluginSnapshotId: null,
      pluginInputs: { prompt: 'Make a launch page for a robotics studio' },
      projectKind: 'other',
    }));
  });

  it('falls back to od-new-generation when od-plugin-authoring is not registered yet', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [DEFAULT_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(DEFAULT_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clickHomeShortcut('create-plugin');
    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/od-new-generation/apply',
      expect.anything(),
    ));
    const applyCall = fetchMock.mock.calls.find(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/od-new-generation/apply')
    ));
    expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
      inputs: {
        artifactKind: 'Open Design plugin',
        audience: 'Open Design plugin authors',
        topic: 'packaging a reusable workflow as an Open Design plugin',
      },
    });
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(PLUGIN_AUTHORING_PROMPT);
      expect((screen.getByTestId('home-hero-submit') as HTMLButtonElement).disabled).toBe(false);
    });
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    expect(screen.queryByRole('alert')).toBeNull();
    expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      prompt: PLUGIN_AUTHORING_PROMPT,
      pluginId: 'od-new-generation',
      appliedPluginSnapshotId: 'snap-default',
      pluginInputs: {
        artifactKind: 'Open Design plugin',
        audience: 'Open Design plugin authors',
        topic: 'packaging a reusable workflow as an Open Design plugin',
      },
      projectKind: 'other',
    }));
  });

  it('binds the Home rail Prototype chip locally and applies it on submit', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(WEB_PROTOTYPE_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        designSystems={[REFLY_DESIGN_SYSTEM]}
        defaultDesignSystemId="ds-refly"
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));

    await waitFor(() => {
      expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
    });
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ))).toBe(false);
    expect(
      screen.getByTestId('home-hero-footer-option-designSystem').textContent,
    ).toContain('Refly Design System');
    expect(screen.getByTestId('home-hero-footer-option-fidelity')).toBeTruthy();
    expect(screen.getByTestId('home-hero-footer-option-designSystem')).toBeTruthy();
    expect(homeHeroPromptValue()).toBe('');
    expect(screen.getByTestId('home-hero-plugin-presets')).toBeTruthy();
    // Inline `{{slot}}` prompt widgets were removed in the Lexical migration;
    // these null checks now confirm the migrated editor never renders them.
    expect(screen.queryByTestId('home-hero-prompt-slot-fidelity')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-artifactKind')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-designSystem')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-template')).toBeNull();
    // The inline plugin inputs form was removed from the Home composer, so the
    // non-footer inputs (artifactKind / audience / template) no longer render;
    // fidelity / designSystem still surface as footer options above.
    expect(screen.queryByTestId('plugin-inputs-form')).toBeNull();

    await setPromptAndSettle('Build a pricing-page prototype.');
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/example-web-prototype/apply',
      expect.anything(),
    ));
    const applyCall = fetchMock.mock.calls.find(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ));
    expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
      inputs: {
        artifactKind: 'web prototype',
        fidelity: 'high-fidelity',
        audience: 'product evaluators',
        designSystem: 'Refly Design System',
        template: 'the bundled web prototype seed',
      },
    });
    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'example-web-prototype',
      projectKind: 'prototype',
      prompt: 'Build a pricing-page prototype.',
      designSystemId: 'ds-refly',
      projectMetadata: expect.objectContaining({
        kind: 'prototype',
        fidelity: 'high-fidelity',
      }),
    })));
    expect(screen.queryByRole('alert')).toBeNull();
  });

  it('uses example preset cards as plain-text prompt fillers while preserving selected chip inputs', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(WEB_PROTOTYPE_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        designSystems={[REFLY_DESIGN_SYSTEM]}
        defaultDesignSystemId="ds-refly"
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
    fireEvent.click(await screen.findByTestId('home-hero-plugin-preset'));

    screen.getByTestId('home-hero-input');
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(
        'Build a high-fidelity web prototype for product evaluators using the active project design system from the bundled web prototype seed.',
      );
    });
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ))).toBe(false);
    expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
    expect(
      screen.getByTestId('home-hero-footer-option-designSystem').textContent,
    ).toContain('Refly Design System');
    expect(screen.getByTestId('home-hero-footer-option-fidelity').textContent).toContain('High fidelity');
    // Inline `{{slot}}` prompt widgets were removed in the Lexical migration.
    expect(screen.queryByTestId('home-hero-prompt-slot-fidelity')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-artifactKind')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-designSystem')).toBeNull();
    expect(screen.queryByTestId('home-hero-prompt-slot-template')).toBeNull();
    // The inline plugin inputs form was removed from the Home composer; the
    // preset card still seeds the prompt and keeps the chip's structured inputs
    // in state (submitted below), but no inputs form renders. fidelity /
    // designSystem stay in the footer options above.
    expect(screen.queryByTestId('plugin-inputs-form')).toBeNull();

    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/example-web-prototype/apply',
      expect.anything(),
    ));
    const applyCall = fetchMock.mock.calls.find(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ));
    // The preset card seeds the prompt as plain text while preserving the
    // chip's structured inputs (artifactKind / fidelity / audience /
    // designSystem / template all round-trip). Seeding the editor does NOT
    // re-run the host's prompt-extraction (HomeHero suppresses the seed echo
    // in onChange), so designSystem keeps the chip/footer default rather than
    // being re-read from the prompt text.
    expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
      inputs: {
        artifactKind: 'web prototype',
        fidelity: 'high-fidelity',
        audience: 'product evaluators',
        designSystem: 'Refly Design System',
        template: 'the bundled web prototype seed',
      },
    });
    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'example-web-prototype',
      projectKind: 'prototype',
      prompt: 'Build a high-fidelity web prototype for product evaluators using the active project design system from the bundled web prototype seed.',
      designSystemId: 'ds-refly',
      projectMetadata: expect.objectContaining({
        kind: 'prototype',
        fidelity: 'high-fidelity',
      }),
    })));
  });

  it('submits live-artifact example presets with chip metadata while keeping them plain-text only', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({
          plugins: [LIVE_ARTIFACT_PLUGIN, LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN],
        }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(LIVE_ARTIFACT_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-live-artifact'));

    await waitFor(() => {
      expect(screen.getAllByTestId('home-hero-plugin-preset').length).toBeGreaterThan(0);
    });
    const liveArtifactTemplatePreset = screen.getAllByTestId('home-hero-plugin-preset')
      .find((item) => item.getAttribute('data-plugin-id') === LIVE_ARTIFACT_IMAGE_TEMPLATE_PLUGIN.id);
    if (!liveArtifactTemplatePreset) {
      throw new Error('expected live artifact image template preset to render');
    }
    fireEvent.click(liveArtifactTemplatePreset);

    screen.getByTestId('home-hero-input');
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe('Create a refreshable Notion dashboard live artifact.');
    });
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/apply')
    ))).toBe(false);
    expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Live artifact');
    expect(screen.queryByTestId('plugin-inputs-form')).toBeNull();

    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/example-live-artifact/apply',
      expect.anything(),
    ));
    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'example-live-artifact',
      appliedPluginSnapshotId: 'snap-live-artifact',
      projectKind: 'prototype',
      projectMetadata: expect.objectContaining({
        kind: 'prototype',
        intent: 'live-artifact',
        fidelity: 'high-fidelity',
      }),
      prompt: 'Create a refreshable Notion dashboard live artifact.',
    })));
  });

  it('binds the Home rail Live artifact chip with live-artifact metadata and applies it on submit', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN, LIVE_ARTIFACT_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/example-live-artifact/apply')) {
        return new Response(JSON.stringify(LIVE_ARTIFACT_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-live-artifact'));

    await waitFor(() => {
      expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Live artifact');
    });
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-live-artifact/apply')
    ))).toBe(false);
    await setPromptAndSettle('Build a refreshable Stripe revenue dashboard.');
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/example-live-artifact/apply',
      expect.anything(),
    ));
    const applyCall = fetchMock.mock.calls.find(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-live-artifact/apply')
    ));
    expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
      inputs: {},
    });
    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'example-live-artifact',
      appliedPluginSnapshotId: 'snap-live-artifact',
      projectKind: 'prototype',
      projectMetadata: expect.objectContaining({
        kind: 'prototype',
        intent: 'live-artifact',
        fidelity: 'high-fidelity',
      }),
      prompt: 'Build a refreshable Stripe revenue dashboard.',
    })));
    expect(screen.queryByRole('alert')).toBeNull();
  });

  it('exposes deck page ranges beside speaker notes and submits the selected range', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [SIMPLE_DECK_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/example-simple-deck/apply')) {
        return new Response(JSON.stringify(SIMPLE_DECK_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        designSystems={[REFLY_DESIGN_SYSTEM]}
        defaultDesignSystemId="ds-refly"
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-deck'));

    await waitFor(() => {
      expect(screen.getByTestId('home-hero-footer-option-speakerNotes')).toBeTruthy();
    });
    expect(screen.getByTestId('home-hero-footer-option-slideCount').textContent).toContain('10-15 pages');

    fireEvent.click(screen.getByTestId('home-hero-footer-option-slideCount'));
    fireEvent.click(await screen.findByRole('option', { name: '15-20 pages' }));
    expect(screen.getByTestId('home-hero-footer-option-slideCount').textContent).toContain('15-20 pages');

    await setPromptAndSettle('Create an investor deck for a local-first design tool.');
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/example-simple-deck/apply',
      expect.anything(),
    ));
    const applyCall = fetchMock.mock.calls.find(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-simple-deck/apply')
    ));
    expect(JSON.parse(String((applyCall?.[1] as RequestInit).body))).toMatchObject({
      inputs: {
        slideCount: '15-20 pages',
        speakerNotes: 'include speaker notes',
        designSystem: 'Refly Design System',
      },
    });
    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'example-simple-deck',
      pluginInputs: expect.objectContaining({
        slideCount: '15-20 pages',
      }),
      projectKind: 'deck',
      projectMetadata: expect.objectContaining({
        kind: 'deck',
        slideCount: '15-20 pages',
        speakerNotes: true,
      }),
    })));
  });

  it('switches output-type chips without replacing an existing prompt', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(DEFAULT_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();

    render(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await screen.findByTestId('home-hero-input');
    await setPromptAndSettle('Keep my current brief');
    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));

    await waitFor(() => {
      expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Prototype');
    });
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ))).toBe(false);
    expect(homeHeroPromptText()).toBe('Keep my current brief');
    expect(screen.queryByRole('dialog', { name: /replace current prompt/i })).toBeNull();
  });

  it('lets selected chips seed the hero through preset cards', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN, SIMPLE_DECK_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')) {
        return new Response(JSON.stringify(WEB_PROTOTYPE_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/api/plugins/example-simple-deck/apply')) {
        return new Response(JSON.stringify(SIMPLE_DECK_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();

    render(
      <HomeView
        projects={[]}
        designSystems={[REFLY_DESIGN_SYSTEM]}
        defaultDesignSystemId="ds-refly"
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-deck'));
    await waitFor(() => {
      expect(screen.getByTestId('home-hero-active-type-chip').textContent).toContain('Slide deck');
    });
    expect(screen.getByTestId('home-hero-plugin-presets')).toBeTruthy();
    expect(screen.getByTestId('home-hero-plugin-presets').textContent).toContain('Simple Deck');
    fireEvent.click(screen.getAllByTestId('home-hero-plugin-preset')[0]!);
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-simple-deck/apply')
    ))).toBe(false);
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(
        'Create a pitch deck for decision makers about the user brief with 10-15 pages. Speaker notes: include speaker notes. Use the active project design system.',
      );
    });

    await clearActiveTypeChip();
    fireEvent.click(await screen.findByTestId('home-hero-rail-prototype'));
    await waitFor(() => {
      expect(screen.getByTestId('home-hero-plugin-presets')).toBeTruthy();
    });
    fireEvent.click(screen.getAllByTestId('home-hero-plugin-preset')[0]!);
    expect(fetchMock.mock.calls.some(([url]) => (
      typeof url === 'string' && url.includes('/api/plugins/example-web-prototype/apply')
    ))).toBe(false);
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(
        'Build a high-fidelity web prototype for product evaluators using the active project design system from the bundled web prototype seed.',
      );
    });
  });

  it('appends a plugin-use query handoff without replacing an existing prompt', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();

    const { rerender } = render(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await screen.findByTestId('home-hero-input');
    await setPromptAndSettle('Keep my current brief');

    rerender(
      <HomeView
        projects={[]}
        onSubmit={() => undefined}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
        promptHandoff={createPluginUseHandoff(2, 'example-web-prototype', {
          action: 'use-with-query',
        })}
      />,
    );

    const expectedPrompt = [
      'Keep my current brief',
      '',
      'Build a high-fidelity web prototype for product evaluators using the active project design system from the bundled web prototype seed.',
    ].join('\n');
    // The caret-at-end assertion (selectionStart/selectionEnd) is not
    // meaningful on a contenteditable; the appended-text behavior is what this
    // test guards, so we keep the text assertion and drop the caret offsets.
    await waitFor(() => {
      expect(homeHeroPromptText()).toBe(expectedPrompt);
    });
    expect(screen.queryByRole('dialog', { name: /replace current prompt/i })).toBeNull();
    // Plugin context stays staged across the appended-prompt handoff; the
    // dropped per-plugin badge is replaced by the active context row's count.
    expect(screen.getByLabelText(/1 context items resolved/i)).toBeTruthy();
    expect(fetchMock.mock.calls.some(([url]) => String(url).includes('/apply'))).toBe(false);
  });

  it('binds od-plugin-authoring before submitting the rail create-plugin prompt', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [AUTHORING_PLUGIN, WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(AUTHORING_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    await clickHomeShortcut('create-plugin');
    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/od-plugin-authoring/apply',
      expect.anything(),
    ));
    await waitFor(() => {
      const badge = screen.getByTestId('home-hero-active-plugin');
      expect(badge.textContent).toContain('Create plugin');
      expect(badge.textContent).not.toContain('Plugin authoring');
    });
    const input = screen.getByTestId('home-hero-input');
    const inputCard = input.closest('.home-hero__input-card') as HTMLElement | null;
    expect(homeHeroPromptText()).toBe(PLUGIN_AUTHORING_PROMPT);
    expect(inputCard?.classList.contains('home-hero__input-card--compact-authoring')).toBe(true);
    expect(inputCard?.style.getPropertyValue('--home-hero-prompt-max-height')).toBe('132px');
    fireEvent.click(await screen.findByTestId('home-hero-submit'));

    expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      prompt: PLUGIN_AUTHORING_PROMPT,
      pluginId: 'od-plugin-authoring',
      appliedPluginSnapshotId: 'snap-authoring',
      pluginInputs: { pluginGoal: PLUGIN_AUTHORING_DEFAULT_GOAL },
      projectKind: 'other',
    }));
  });

  it('keeps the authoring goal input linked to the prompt and submit payload', async () => {
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [AUTHORING_PLUGIN, WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return new Response(JSON.stringify(AUTHORING_APPLY_RESULT), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    await clickHomeShortcut('create-plugin');
    await waitFor(() => expect(fetchMock).toHaveBeenCalledWith(
      '/api/plugins/od-plugin-authoring/apply',
      expect.anything(),
    ));

    const rewrittenGoal = 'catalog internal research notes into a reusable knowledge workflow';
    screen.getByTestId('home-hero-input');
    await setPromptAndSettle(
      homeHeroPromptText().replace(PLUGIN_AUTHORING_DEFAULT_GOAL, rewrittenGoal),
    );
    await waitFor(() => {
      expect(homeHeroPromptText()).toContain(rewrittenGoal);
    });
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    await waitFor(() => expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      prompt: expect.stringContaining(rewrittenGoal),
      pluginId: 'od-plugin-authoring',
      pluginInputs: {
        pluginGoal: rewrittenGoal,
      },
    })));
  });

  it('does not submit the create-plugin prompt before the authoring scenario is applied', async () => {
    let resolveApply: (response: Response) => void = () => undefined;
    const applyResponse = new Promise<Response>((resolve) => {
      resolveApply = resolve;
    });
    const fetchMock = vi.fn<typeof fetch>(async (url) => {
      if (typeof url === 'string' && url === '/api/plugins') {
        return new Response(JSON.stringify({ plugins: [AUTHORING_PLUGIN, WEB_PROTOTYPE_PLUGIN] }), {
          status: 200,
          headers: { 'content-type': 'application/json' },
        });
      }
      if (typeof url === 'string' && url.includes('/apply')) {
        return applyResponse;
      }
      throw new Error(`unexpected fetch ${url}`);
    });
    vi.stubGlobal('fetch', fetchMock);
    stubAnimationFrame();
    const onSubmit = vi.fn();

    render(
      <HomeView
        projects={[]}
        onSubmit={onSubmit}
        onOpenProject={() => undefined}
        onViewAllProjects={() => undefined}
      />,
    );

    await clearActiveTypeChip();
    await clickHomeShortcut('create-plugin');
    const input = screen.getByTestId('home-hero-input');
    const inputCard = input.closest('.home-hero__input-card') as HTMLElement | null;
    expect(homeHeroPromptText()).toBe(PLUGIN_AUTHORING_PROMPT);
    expect(inputCard?.classList.contains('home-hero__input-card--compact-authoring')).toBe(true);
    expect(inputCard?.style.getPropertyValue('--home-hero-prompt-max-height')).toBe('132px');
    fireEvent.click(await screen.findByTestId('home-hero-submit'));
    expect(onSubmit).not.toHaveBeenCalled();

    resolveApply(new Response(JSON.stringify(AUTHORING_APPLY_RESULT), {
      status: 200,
      headers: { 'content-type': 'application/json' },
    }));
    await waitFor(() => {
      expect((screen.getByTestId('home-hero-submit') as HTMLButtonElement).disabled).toBe(false);
    });
    fireEvent.click(screen.getByTestId('home-hero-submit'));

    expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({
      pluginId: 'od-plugin-authoring',
      appliedPluginSnapshotId: 'snap-authoring',
    }));
  });
});

// An empty Lexical editor renders `<p><br></p>` (a placeholder break node), so
// the DOM serializer in `homeHeroPromptText()` reads that lone `<br>` back as
// `'\n'`. The editor's real text is empty — `.textContent` is `''` — so this
// reads the empty case precisely without weakening the genuine-content path.
function homeHeroPromptValue(): string {
  const text = homeHeroPromptText();
  if (text === '\n' && (screen.getByTestId('home-hero-input').textContent ?? '') === '') {
    return '';
  }
  return text;
}

// Replace the Lexical editor's text the way a user edit would, then let the
// editor's OnChange → host `onPromptChange` React state update flush a
// microtask (mirrors lexical-composer's `typeAndSettle`) so flows that submit
// right after editing read the latest draft.
async function setPromptAndSettle(value: string): Promise<void> {
  setHomeHeroPrompt(value);
  await act(async () => {
    await Promise.resolve();
  });
}

async function clearActiveTypeChip() {
  const chip = screen.queryByTestId('home-hero-active-type-chip');
  if (chip) fireEvent.click(chip);
}

async function clickHomeShortcut(id: string) {
  const trigger = await screen.findByTestId('home-hero-shortcuts-trigger');
  await waitFor(() => expect((trigger as HTMLButtonElement).disabled).toBe(false));
  fireEvent.click(trigger);
  fireEvent.click(await screen.findByTestId(`home-hero-rail-${id}`));
}
