import { describe, test, expect, afterEach } from 'vitest';
import React from 'react';
import { renderEditorHarness, EditorHarness } from '../editorHarness';
import { useApplyAiResponse } from '../../utils/apply-ai-response';
import { exportBodyHtml } from '../../utils/html-export';
import type { SitesmithResponse } from '../../types/sitesmith';

/**
 * Real-`@craftjs/core` integration coverage for applying a Sitesmith (AI)
 * response.
 *
 * Historical bug: `buildNodeTree` used to hand `query.parseFreshNode`/the
 * manually-constructed synthetic node a `data.type` of the SERIALIZED
 * `{ resolvedName }` wrapper object (or, in an earlier attempt, a bare
 * resolvedName string) instead of the actual component function reference
 * from `componentResolver`. A live Craft.js node's renderer does
 * `React.createElement(data.type, props)` directly, so anything other than
 * a real component reference either fails Craft's own resolver validation
 * inside `parseFreshNode`/`addNodeTree` ("component type ... does not exist
 * in the resolver") or -- worse -- passes that validation but renders
 * nothing/garbage. Either way, a section-replace or insert AI op became a
 * silent no-op in the live app. The pre-existing mocked unit test
 * (`apply-ai-response.test.ts`) fakes `query.parseFreshNode` as
 * `(input) => ({ toNode: () => input })` -- a pure echo with NO validation
 * at all -- so it could never have caught `buildNodeTree` emitting the
 * wrong `data.type` shape.
 *
 * This suite drives the REAL `useApplyAiResponse()` hook (the exact
 * consumer-facing API `SitesmithModal`/`useSitesmith` call after a
 * successful AI response) against a real `EditorStore`/`<Frame>` from
 * `editorHarness.tsx` -- so a regression in `buildNodeTree`'s `data.type`
 * would throw here (real `parseFreshNode` resolver validation) or fail the
 * "content actually landed" assertions (real render/export), exactly as it
 * did live.
 */

const INITIAL_STATE = JSON.stringify({
  ROOT: {
    type: { resolvedName: 'Container' },
    isCanvas: true,
    props: { style: {}, tag: 'div' },
    displayName: 'Container',
    custom: {},
    hidden: false,
    nodes: ['anchor-1'],
    linkedNodes: {},
  },
  'anchor-1': {
    type: { resolvedName: 'Heading' },
    isCanvas: false,
    props: { text: 'Anchor Heading', level: 'h2' },
    displayName: 'Heading',
    custom: {},
    hidden: false,
    parent: 'ROOT',
    nodes: [],
    linkedNodes: {},
  },
});

let harness: EditorHarness | null = null;

afterEach(() => {
  if (harness) {
    harness.unmount();
    harness = null;
  }
});

/** Mounts `useApplyAiResponse()` (the real hook) inside the harness's live
 *  Editor and returns the captured `apply` function. */
function mountApplyFn(h: EditorHarness): { current: ReturnType<typeof useApplyAiResponse> | null } {
  const ref: { current: ReturnType<typeof useApplyAiResponse> | null } = { current: null };
  const Probe: React.FC = () => {
    ref.current = useApplyAiResponse();
    return null;
  };
  h.mountChild(<Probe />);
  return ref;
}

describe('AI apply-response (real @craftjs/core editor)', () => {
  test('section-scope replace with no target: appends a real, rendered node -- not a silent no-op', async () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });
    const applyRef = mountApplyFn(harness);
    expect(applyRef.current).not.toBeNull();

    const resp: SitesmithResponse = {
      type: 'replace',
      scope: 'section',
      pages: [
        {
          name: 'section',
          tree: { type: { resolvedName: 'Heading' }, props: { text: 'AI Inserted Heading Panel' }, nodes: [] },
        },
      ],
      message: 'inserted a section',
    };

    let result: { ok: boolean; message?: string } | undefined;
    await harness.act(async () => {
      result = await applyRef.current!(resp);
    });

    expect(result!.ok).toBe(true);

    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toHaveLength(2);
    expect(rootChildren).toContain('anchor-1');
    const newId = rootChildren.find((id) => id !== 'anchor-1')!;
    expect(newId).toBeDefined();

    // Real node.data.type must be the actual component reference so React
    // can render it -- not a `{resolvedName}` wrapper or a bare string.
    const newNodeType = harness.query.node(newId).get().data.type;
    expect(typeof newNodeType).toBe('function');

    expect(harness.container.textContent).toContain('AI Inserted Heading Panel');

    const { html } = exportBodyHtml(harness.getSerialized());
    expect(html).toContain('AI Inserted Heading Panel');
  });

  test('section-scope replace WITH a target node: replaces that node in place (same slot, fresh id)', async () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });
    const applyRef = mountApplyFn(harness);

    const resp: SitesmithResponse = {
      type: 'replace',
      scope: 'section',
      pages: [
        {
          name: 'section',
          tree: { type: { resolvedName: 'Heading' }, props: { text: 'AI Replaced The Anchor' }, nodes: [] },
        },
      ],
      message: 'replaced the section',
    };

    await harness.act(async () => {
      await applyRef.current!(resp, 'anchor-1');
    });

    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toHaveLength(1); // still just one node in that slot
    expect(rootChildren).not.toContain('anchor-1'); // the old node is gone

    expect(harness.container.textContent).toContain('AI Replaced The Anchor');
    expect(harness.container.textContent).not.toContain('Anchor Heading');
  });

  test('patch op insert_after: inserts a real node as the very next sibling', async () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });
    const applyRef = mountApplyFn(harness);

    const resp: SitesmithResponse = {
      type: 'patch',
      ops: [
        {
          op: 'insert_after',
          node_id: 'anchor-1',
          tree: { type: { resolvedName: 'Heading' }, props: { text: 'AI Patched In After Anchor' }, nodes: [] },
        },
      ],
      message: 'patched',
    };

    let result: { ok: boolean; message?: string } | undefined;
    await harness.act(async () => {
      result = await applyRef.current!(resp);
    });

    expect(result!.ok).toBe(true);

    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toHaveLength(2);
    expect(rootChildren[0]).toBe('anchor-1');
    const insertedId = rootChildren[1];

    expect(typeof harness.query.node(insertedId).get().data.type).toBe('function');
    expect(harness.container.textContent).toContain('AI Patched In After Anchor');
    // The original anchor is untouched.
    expect(harness.container.textContent).toContain('Anchor Heading');
  });
});
