import { describe, test, expect, afterEach } from 'vitest';
import React from 'react';
import { renderEditorHarness, EditorHarness } from '../editorHarness';
import { useNodeActions, NodeActions } from '../../hooks/useNodeActions';
import { useKeyboardShortcuts } from '../../hooks/useKeyboardShortcuts';
import { getClipboardTree, setClipboardTree } from '../../hooks/clipboard';

/**
 * Real-`@craftjs/core` integration coverage for duplicate/paste.
 *
 * Historical bug: `regenerateTreeIds` used `structuredClone(oldNode.data)` to
 * clone a copied subtree. For a REAL (live) Craft.js node, `data.type` is the
 * actual component function reference (not a plain `{resolvedName}` string
 * wrapper) -- `structuredClone` cannot clone a function and throws
 * `DataCloneError`, which silently broke duplicate/paste for EVERY
 * component in the real app. The pre-existing mocked unit tests
 * (`useNodeActions.test.tsx`, `useKeyboardShortcuts.test.tsx`) faked
 * `query.node(id).toNodeTree()` to return a plain-data object with
 * `type: { resolvedName: 'Container' }` -- never a function -- so
 * `structuredClone` never saw anything it couldn't clone there, and the bug
 * shipped anyway.
 *
 * This suite drives the REAL `useNodeActions`/`useKeyboardShortcuts` hooks
 * (no `vi.mock('@craftjs/core')` anywhere) against a real
 * `EditorStore`/`query`/`actions` from `editorHarness.tsx`, on a real
 * `SocialLinks` node (its `.craft` config makes Craft.js store the actual
 * `SocialLinks` function as `data.type`) -- so a regression back to
 * `structuredClone(oldNode.data)` throws here exactly as it did live. See
 * `task-integ-harness-report.md` for the recorded red-proof run.
 */

const INITIAL_STATE = JSON.stringify({
  ROOT: {
    type: { resolvedName: 'Container' },
    isCanvas: true,
    props: { style: {}, tag: 'div' },
    displayName: 'Container',
    custom: {},
    hidden: false,
    nodes: ['social-1', 'heading-1'],
    linkedNodes: {},
  },
  'social-1': {
    type: { resolvedName: 'SocialLinks' },
    isCanvas: false,
    props: {
      links: [{ platform: 'facebook', url: 'https://facebook.com/original' }],
      iconColor: '#ffffff',
    },
    displayName: 'Social Links',
    custom: {},
    hidden: false,
    parent: 'ROOT',
    nodes: [],
    linkedNodes: {},
  },
  'heading-1': {
    type: { resolvedName: 'Heading' },
    isCanvas: false,
    props: { text: 'Sibling Heading', level: 'h2' },
    displayName: 'Heading',
    custom: {},
    hidden: false,
    parent: 'ROOT',
    nodes: [],
    linkedNodes: {},
  },
});

// A second, independent "page" state -- distinct node ids from INITIAL_STATE,
// simulating what PageContext.switchPage does: `query.serialize()` the
// current page, then `actions.deserialize()` the target page's stored
// state, replacing the ENTIRE node map. `social-1` (copied from page A)
// does not exist anywhere in this state.
const PAGE_B_STATE = JSON.stringify({
  ROOT: {
    type: { resolvedName: 'Container' },
    isCanvas: true,
    props: { style: {}, tag: 'div' },
    displayName: 'Container',
    custom: {},
    hidden: false,
    nodes: ['page-b-heading-1'],
    linkedNodes: {},
  },
  'page-b-heading-1': {
    type: { resolvedName: 'Heading' },
    isCanvas: false,
    props: { text: 'Page B Heading', level: 'h2' },
    displayName: 'Heading',
    custom: {},
    hidden: false,
    parent: 'ROOT',
    nodes: [],
    linkedNodes: {},
  },
});

let harness: EditorHarness | null = null;

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

describe('duplicate/paste (real @craftjs/core editor)', () => {
  test('useNodeActions().duplicate() on a real SocialLinks node: no throw, +1 node, inserted right after the source, with an independent props copy', () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });

    const before = Object.keys(harness.query.getNodes());
    expect(before).toHaveLength(3); // ROOT, social-1, heading-1

    const probeRef: { current: NodeActions | null } = { current: null };
    const Probe: React.FC = () => {
      probeRef.current = useNodeActions('social-1');
      return null;
    };
    harness.mountChild(<Probe />);
    expect(probeRef.current).not.toBeNull();

    let newId: string | null = null;
    expect(() => {
      harness!.act(() => {
        newId = probeRef.current!.duplicate();
      });
    }).not.toThrow();

    expect(newId).not.toBeNull();
    expect(newId).not.toBe('social-1');

    // +1 node overall.
    const after = Object.keys(harness.query.getNodes());
    expect(after).toHaveLength(4);

    // Inserted immediately AFTER the source, not appended at the end.
    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toEqual(['social-1', newId, 'heading-1']);

    // The new node was selected.
    expect(Array.from(harness.query.getEvent('selected').all())).toEqual([newId]);

    // The copy's props object (and any nested arrays/objects within it) must
    // be a SEPARATE object from the original -- not shared by reference.
    const originalProps = harness.query.node('social-1').get().data.props;
    const copyProps = harness.query.node(newId!).get().data.props;
    expect(copyProps).not.toBe(originalProps);
    expect(copyProps.links).not.toBe(originalProps.links);
    expect(copyProps).toEqual(originalProps); // same VALUES though

    // Mutating the copy via setProp must not affect the original.
    harness.act(() => {
      harness!.actions.setProp(newId!, (p: any) => {
        p.links[0].url = 'https://facebook.com/MUTATED-COPY';
      });
    });
    expect(harness.query.node(newId!).get().data.props.links[0].url).toBe(
      'https://facebook.com/MUTATED-COPY',
    );
    expect(harness.query.node('social-1').get().data.props.links[0].url).toBe(
      'https://facebook.com/original',
    );

    // Real DOM assertion, not just serialized state: the Frame actually
    // re-rendered with a second SocialLinks <a> link on the canvas.
    expect(harness.container.querySelectorAll('a[href="https://facebook.com/original"]')).toHaveLength(1);
    expect(harness.container.querySelectorAll('a[href="https://facebook.com/MUTATED-COPY"]')).toHaveLength(1);
  });

  test('Ctrl+C / Ctrl+V (useKeyboardShortcuts) on the real editor: pastes a fresh-id sibling copy, original untouched', () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });

    const Consumer: React.FC = () => {
      useKeyboardShortcuts();
      return null;
    };
    harness.mountChild(<Consumer />);

    // Select + copy social-1.
    harness.act(() => {
      harness!.actions.selectNode('social-1');
    });
    harness.act(() => {
      document.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'c', ctrlKey: true, bubbles: true, cancelable: true }),
      );
    });
    expect(getClipboardTree()!.rootNodeId).toBe('social-1');

    // Select heading-1 (a sibling), then paste -- should land as a sibling
    // of heading-1's parent (ROOT), with brand-new ids.
    harness.act(() => {
      harness!.actions.selectNode('heading-1');
    });

    const before = Object.keys(harness.query.getNodes());
    expect(() => {
      harness!.act(() => {
        document.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'v', ctrlKey: true, bubbles: true, cancelable: true }),
        );
      });
    }).not.toThrow();

    const after = Object.keys(harness.query.getNodes());
    expect(after).toHaveLength(before.length + 1);

    const pastedId = after.find((id) => !before.includes(id))!;
    expect(pastedId).toBeDefined();

    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toContain(pastedId);

    // Independent props copy again -- same class of bug, same guard.
    const pastedProps = harness.query.node(pastedId).get().data.props;
    const originalProps = harness.query.node('social-1').get().data.props;
    expect(pastedProps.links).not.toBe(originalProps.links);
    expect(pastedProps).toEqual(originalProps);
  });

  test('CROSS-PAGE copy/paste: copy on page A, switch to page B, paste -- the node appears on page B', () => {
    harness = renderEditorHarness({ initialState: INITIAL_STATE });

    const Consumer: React.FC = () => {
      useKeyboardShortcuts();
      return null;
    };
    harness.mountChild(<Consumer />);

    // --- Page A: select + copy social-1. ---
    harness.act(() => {
      harness!.actions.selectNode('social-1');
    });
    harness.act(() => {
      document.dispatchEvent(
        new KeyboardEvent('keydown', { key: 'c', ctrlKey: true, bubbles: true, cancelable: true }),
      );
    });
    expect(getClipboardTree()!.rootNodeId).toBe('social-1');

    // --- Switch to page B: exactly what PageContext.switchPage does --
    // serialize (discarded here, a real page switch would stash it) then
    // deserialize the target page's state, replacing the ENTIRE node map.
    // `social-1` no longer exists anywhere in `query` after this. ---
    harness.act(() => {
      harness!.actions.deserialize(PAGE_B_STATE);
    });
    expect(harness.query.getNodes()['social-1']).toBeUndefined();

    // Select page B's only node, then paste.
    harness.act(() => {
      harness!.actions.selectNode('page-b-heading-1');
    });

    const beforePaste = Object.keys(harness.query.getNodes());
    expect(() => {
      harness!.act(() => {
        document.dispatchEvent(
          new KeyboardEvent('keydown', { key: 'v', ctrlKey: true, bubbles: true, cancelable: true }),
        );
      });
    }).not.toThrow();

    const afterPaste = Object.keys(harness.query.getNodes());

    // The regression this guards against: with the old id-based clipboard,
    // `query.node('social-1').get()` returns undefined once page B is
    // loaded, so the paste handler's guard silently no-ops -- NOTHING gets
    // added. With the tree-snapshot clipboard, the copied subtree is
    // detached from any live query and pastes onto page B regardless.
    expect(afterPaste.length).toBe(beforePaste.length + 1);

    const pastedId = afterPaste.find((id) => !beforePaste.includes(id))!;
    expect(pastedId).toBeDefined();

    const pastedNode = harness.query.node(pastedId).get();
    expect(pastedNode.data.displayName).toBe('Social Links');
    expect(pastedNode.data.props.links).toEqual([
      { platform: 'facebook', url: 'https://facebook.com/original' },
    ]);

    // Actually landed on page B's tree, as a sibling of the selected node.
    const rootChildren: string[] = harness.query.node('ROOT').get().data.nodes;
    expect(rootChildren).toContain(pastedId);

    // Real DOM assertion: the pasted SocialLinks component is actually
    // rendered on the (now page B) canvas.
    expect(
      harness.container.querySelectorAll('a[href="https://facebook.com/original"]'),
    ).toHaveLength(1);
  });
});
