import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import type { NodeTree, Node } from '@craftjs/core';
import { useNodeActions, type NodeActions } from './useNodeActions';

/**
 * Phase B, item 1: `useNodeActions` was extracted out of `ContextMenu.tsx` so
 * the desktop right-click menu and the mobile selection toolbar
 * (`MobileSelectionToolbar`) share one implementation. This suite exercises
 * the hook directly (mocking `@craftjs/core`'s `useEditor`, mirroring the
 * pattern in `useKeyboardShortcuts.test.tsx`), covering:
 *  - moveUp/moveDown call `actions.move` with the right target index, and
 *    are no-ops at the respective boundary
 *  - canMoveUp/canMoveDown reflect the node's position among its siblings
 *  - duplicate regenerates ids, inserts the copy IMMEDIATELY AFTER the
 *    source node (`actions.addNodeTree(tree, parentId, sourceIndex + 1)`),
 *    and selects the new copy (`actions.selectNode(tree.rootNodeId)`) --
 *    Phase B fast-follow: the previous append-at-end behavior left the copy
 *    off-screen with the ORIGINAL still selected, so users couldn't see
 *    what "Duplicate" had just done.
 *  - selectParent calls `actions.selectNode` with the parent id
 *  - canSelectParent reflects whether the node's parent is itself real
 *    (not ROOT/missing) -- false at a top-level section, true one level in
 *  - deleteNode routes through `findDeletableTarget` (also covering the
 *    "no deletable target" no-op)
 *  - ROOT/null nodeId disables every action safely (no-ops, all
 *    canMoveUp/canMoveDown/canDelete/canSelectParent false)
 */

const moveMock = vi.fn();
const addNodeTreeMock = vi.fn();
const selectNodeMock = vi.fn();
const deleteMock = vi.fn();

interface FakeNodeData {
  parent: string | null;
  nodes: string[];
}

let fakeNodes: Record<string, { data: FakeNodeData }> = {};

const COPIED_TREE: NodeTree = {
  rootNodeId: 'child-1',
  nodes: {
    'child-1': {
      id: 'child-1',
      data: { props: {}, type: { resolvedName: 'Container' }, name: 'Container', displayName: 'Container', isCanvas: false, parent: 'parent-1', linkedNodes: {}, nodes: [], hidden: false },
      info: {},
      events: { selected: false, dragged: false, hovered: false },
      dom: null,
      related: {},
      rules: {},
      _hydrationTimestamp: 0,
    } as unknown as Node,
  },
};

function makeQuery() {
  return {
    node: (id: string) => ({
      get: () => fakeNodes[id] ?? null,
      toNodeTree: () => {
        if (id !== 'child-1') throw new Error(`unexpected toNodeTree() for "${id}"`);
        return COPIED_TREE;
      },
    }),
  };
}

vi.mock('@craftjs/core', () => ({
  useEditor: (collector?: (state: any, query: any) => any) => {
    const state = {
      nodes: Object.fromEntries(Object.entries(fakeNodes).map(([id, n]) => [id, n])),
    };
    const query = makeQuery();
    const collected = collector ? collector(state, query) : {};
    return {
      ...collected,
      actions: {
        move: moveMock,
        addNodeTree: addNodeTreeMock,
        selectNode: selectNodeMock,
        delete: deleteMock,
      },
      query,
    };
  },
}));

vi.mock('../utils/craft-tree', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../utils/craft-tree')>();
  return { ...actual, regenerateTreeIds: vi.fn(actual.regenerateTreeIds) };
});

import { regenerateTreeIds } from '../utils/craft-tree';
const regenerateTreeIdsMock = vi.mocked(regenerateTreeIds);

let container: HTMLDivElement;
let root: Root;
let captured: NodeActions | null = null;

const Probe: React.FC<{ nodeId: string | null }> = ({ nodeId }) => {
  captured = useNodeActions(nodeId);
  return null;
};

function render(nodeId: string | null) {
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
    root.render(<Probe nodeId={nodeId} />);
  });
}

function rerender(nodeId: string | null) {
  act(() => {
    root.render(<Probe nodeId={nodeId} />);
  });
}

function unmount() {
  act(() => {
    root.unmount();
  });
  container.remove();
}

beforeEach(() => {
  moveMock.mockClear();
  addNodeTreeMock.mockClear();
  selectNodeMock.mockClear();
  deleteMock.mockClear();
  regenerateTreeIdsMock.mockClear();
  captured = null;
  fakeNodes = {
    ROOT: { data: { parent: null, nodes: ['parent-1'] } },
    'parent-1': { data: { parent: 'ROOT', nodes: ['child-0', 'child-1', 'child-2'] } },
    'child-0': { data: { parent: 'parent-1', nodes: [] } },
    'child-1': { data: { parent: 'parent-1', nodes: [] } },
    'child-2': { data: { parent: 'parent-1', nodes: [] } },
  };
});

afterEach(() => {
  if (root) unmount();
});

describe('useNodeActions', () => {
  test('a middle child can move both up and down', () => {
    render('child-1');
    expect(captured!.canMoveUp).toBe(true);
    expect(captured!.canMoveDown).toBe(true);
  });

  test('the first child cannot move up, but can move down', () => {
    render('child-0');
    expect(captured!.canMoveUp).toBe(false);
    expect(captured!.canMoveDown).toBe(true);
  });

  test('the last child can move up, but not down', () => {
    render('child-2');
    expect(captured!.canMoveUp).toBe(true);
    expect(captured!.canMoveDown).toBe(false);
  });

  test('moveUp calls actions.move with idx - 1, moveDown with idx + 2 (Craft.js index semantics)', () => {
    render('child-1');
    act(() => captured!.moveUp());
    expect(moveMock).toHaveBeenCalledWith('child-1', 'parent-1', 0);

    moveMock.mockClear();
    act(() => captured!.moveDown());
    expect(moveMock).toHaveBeenCalledWith('child-1', 'parent-1', 3);
  });

  test('moveUp/moveDown at a boundary are no-ops', () => {
    render('child-0');
    act(() => captured!.moveUp());
    expect(moveMock).not.toHaveBeenCalled();

    rerender('child-2');
    act(() => captured!.moveDown());
    expect(moveMock).not.toHaveBeenCalled();
  });

  test('duplicate inserts the copy immediately after the source (parentId, sourceIndex + 1) and selects it', () => {
    render('child-1');
    let returned: string | null | undefined;
    act(() => {
      returned = captured!.duplicate();
    });

    expect(regenerateTreeIdsMock).toHaveBeenCalledTimes(1);
    expect(addNodeTreeMock).toHaveBeenCalledTimes(1);
    const [tree, parentId, index] = addNodeTreeMock.mock.calls[0];
    // child-1 is at index 1 among ['child-0', 'child-1', 'child-2'] -> insert at 2.
    expect(parentId).toBe('parent-1');
    expect(index).toBe(2);
    expect(tree.rootNodeId).not.toBe(COPIED_TREE.rootNodeId);

    expect(selectNodeMock).toHaveBeenCalledTimes(1);
    expect(selectNodeMock).toHaveBeenCalledWith(tree.rootNodeId);
    expect(returned).toBe(tree.rootNodeId);
  });

  test('selectParent calls actions.selectNode with the parent id', () => {
    render('child-1');
    act(() => captured!.selectParent());
    expect(selectNodeMock).toHaveBeenCalledWith('parent-1');
  });

  test('canSelectParent is false at a top-level node (parent is ROOT), true one level deeper', () => {
    render('parent-1'); // parent-1's own parent is 'ROOT'
    expect(captured!.canSelectParent).toBe(false);

    rerender('child-1'); // child-1's parent is 'parent-1', a real node
    expect(captured!.canSelectParent).toBe(true);
  });

  test('deleteNode calls actions.delete with the node id when deletable', () => {
    render('child-1');
    expect(captured!.canDelete).toBe(true);
    act(() => captured!.deleteNode());
    expect(deleteMock).toHaveBeenCalledWith('child-1');
  });

  test('ROOT nodeId disables every action and is a safe no-op', () => {
    render('ROOT');
    expect(captured!.canMoveUp).toBe(false);
    expect(captured!.canMoveDown).toBe(false);
    expect(captured!.canDelete).toBe(false);
    expect(captured!.canSelectParent).toBe(false);

    act(() => {
      captured!.moveUp();
      captured!.moveDown();
      captured!.duplicate();
      captured!.selectParent();
      captured!.deleteNode();
    });
    expect(moveMock).not.toHaveBeenCalled();
    expect(addNodeTreeMock).not.toHaveBeenCalled();
    expect(selectNodeMock).not.toHaveBeenCalled();
    expect(deleteMock).not.toHaveBeenCalled();
  });

  test('null nodeId disables every action and is a safe no-op', () => {
    render(null);
    expect(captured!.canMoveUp).toBe(false);
    expect(captured!.canMoveDown).toBe(false);
    expect(captured!.canDelete).toBe(false);
    expect(captured!.canSelectParent).toBe(false);
  });
});
