import { describe, test, expect } from 'vitest';
import React from 'react';
import { renderEditorHarness } from '../../test-utils/editorHarness';
import { LayersPanel } from './LayersPanel';
import { LayerFocusProvider } from './LayerFocusContext';

function stateWith(extra: Record<string, any>, rootChildren: string[]) {
  return JSON.stringify({
    ROOT: {
      type: { resolvedName: 'Container' }, isCanvas: true,
      props: { style: {}, tag: 'div' }, displayName: 'Container',
      custom: {}, hidden: false, nodes: rootChildren, linkedNodes: {}, parent: null,
    },
    ...extra,
  });
}

const featuresNode = {
  type: { resolvedName: 'FeaturesGrid' }, isCanvas: false,
  props: { features: [{ title: 'Fast' }, { title: 'Secure' }] },
  displayName: 'Features Grid',
  custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
};

describe('LayersPanel', () => {
  test('shows virtual rows for a Features Grid array prop', () => {
    const harness = renderEditorHarness({ initialState: stateWith({ f1: featuresNode }, ['f1']) });
    harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);

    // Scoped to `.layer-virtual-row` rather than the whole container:
    // `FeaturesGrid` itself renders `feat.title` on the live canvas (which
    // is also mounted inside `harness.container`, alongside the panel), so
    // asserting on `container.textContent` alone would pass even if
    // LayersPanel never rendered a single virtual row -- the "Fast"/"Secure"
    // text would already be there from the canvas. Scoping to the virtual
    // row elements themselves makes the assertion actually exercise
    // LayersPanel's own rendering.
    const virtualRows = harness.container.querySelectorAll('.layer-virtual-row');
    expect(virtualRows).toHaveLength(2);
    const virtualText = Array.from(virtualRows).map((el) => el.textContent).join(' | ');
    expect(virtualText).toContain('Fast');
    expect(virtualText).toContain('Secure');

    // The real node row for the parent is still shown too.
    const nodeRows = harness.container.querySelectorAll('.layer-node-row');
    const nodeText = Array.from(nodeRows).map((el) => el.textContent).join(' | ');
    expect(nodeText).toContain('Features Grid');

    harness.unmount();
  });

  test('virtual rows are not rendered for a component with no registry entry', () => {
    const heading = {
      type: { resolvedName: 'Heading' }, isCanvas: false,
      props: { text: 'Title', level: 2 }, displayName: 'Heading',
      custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
    };
    const harness = renderEditorHarness({ initialState: stateWith({ h1: heading }, ['h1']) });
    harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
    expect(harness.container.querySelectorAll('.layer-virtual-row')).toHaveLength(0);
    harness.unmount();
  });

  test('the Unplaced group does not render for a healthy tree', () => {
    const harness = renderEditorHarness({ initialState: stateWith({ f1: featuresNode }, ['f1']) });
    harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);
    expect(harness.container.textContent).not.toContain('Unplaced');
    harness.unmount();
  });

  test('the Unplaced group appears and lists a node unreachable from ROOT', () => {
    // 'stray' is a real node in the deserialized state, but no node's
    // `nodes`/`linkedNodes` list references it -- exactly the "dropped
    // outside the page" scenario findUnreachableNodeIds exists to catch.
    const heading = {
      type: { resolvedName: 'Heading' }, isCanvas: false,
      props: { text: 'Title', level: 2 }, displayName: 'Heading',
      custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ROOT',
    };
    const stray = {
      type: { resolvedName: 'HtmlBlock' }, isCanvas: false,
      props: { code: '<p>stranded</p>', style: {} }, displayName: 'HTML',
      custom: {}, hidden: false, nodes: [], linkedNodes: {}, parent: 'ghost',
    };
    const harness = renderEditorHarness({
      initialState: stateWith({ h1: heading, stray }, ['h1']),
    });
    harness.mountChild(<LayerFocusProvider><LayersPanel /></LayerFocusProvider>);

    expect(harness.container.textContent).toContain('Unplaced');
    expect(harness.container.textContent).toContain('Unplaced (1)');

    // The orphan itself is rendered as a selectable/deletable LayerNode row
    // (displayName 'HTML') underneath the Unplaced heading, not silently
    // dropped from the tree.
    const nodeRows = harness.container.querySelectorAll('.layer-node-row');
    const nodeText = Array.from(nodeRows).map((el) => el.textContent).join(' | ');
    expect(nodeText).toContain('HTML');

    harness.unmount();
  });
});
