import React, { act, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import type { Root } from 'react-dom/client';
import { ProjectProvider, useProject } from '../../context/ProjectContext';
import type { ProjectDocument } from '../../types/project';
import ActionInspector from './ActionInspector';
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;


function ProjectProbe(): React.ReactElement {
  const { doc, isDirty } = useProject();
  return (
    <>
      <pre data-testid="project-probe">{JSON.stringify(doc)}</pre>
      <span data-testid="dirty-probe">{String(isDirty)}</span>
    </>
  );
}

function ProjectSeed({ legacyBinding = false }: { legacyBinding?: boolean }): null {
  const { setDoc } = useProject();
  useEffect(() => setDoc((doc) => ({
    ...doc,
    project: {
      ...doc.project,
      pages: [{ ...doc.project.pages[0], components: [
        { id: 'cmp_result', type: 'JsonViewer', name: 'result', position: { x: 0, y: 0 }, size: { width: 200, height: 100 }, properties: {} },
      ] }],
      actions: [{ id: 'action_lookup', name: 'Lookup', method: 'GET', url: 'https://example.com', headers: {}, queryParameters: {}, pathParameters: {}, bodyTemplate: '', authenticationType: 'anonymous' }],
      bindings: legacyBinding ? [{ id: 'binding_legacy', source: 'actions.action_lookup.response.body', target: 'components.result.value', trigger: 'onClick' }] : [],
    },
  })), [setDoc]);
  return null;
}

function currentDocument(container: HTMLElement): ProjectDocument {
  const probe = container.querySelector('[data-testid="project-probe"]');
  if (!probe?.textContent) throw new Error('Project probe was not rendered.');
  return JSON.parse(probe.textContent) as ProjectDocument;
}

function click(element: Element | null): void {
  if (!element) throw new Error('Expected clickable element was not found.');
  act(() => {
    element.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  });
}

function doneButton(container: HTMLElement): HTMLButtonElement {
  const button = container.querySelector<HTMLButtonElement>('button[type="submit"]');
  if (!button) throw new Error('Done button was not found.');
  return button;
}

function setControlValue(
  element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement | null,
  value: string,
): void {
  if (!element) throw new Error('Expected form control was not found.');

  const prototype = element instanceof HTMLInputElement
    ? HTMLInputElement.prototype
    : element instanceof HTMLTextAreaElement
      ? HTMLTextAreaElement.prototype
      : HTMLSelectElement.prototype;
  const setter = Object.getOwnPropertyDescriptor(prototype, 'value')?.set;
  if (!setter) throw new Error('Could not find native value setter.');

  act(() => {
    setter.call(element, value);
    element.dispatchEvent(new Event(
      element instanceof HTMLSelectElement ? 'change' : 'input',
      { bubbles: true },
    ));
  });
}

describe('Actions & Bindings visual REST action authoring', () => {
  let container: HTMLDivElement;
  let root: Root;

  beforeEach(() => {
    container = document.createElement('div');
    document.body.appendChild(container);
    root = createRoot(container);

    act(() => {
      root.render(
        <ProjectProvider>
          <ActionInspector />
          <ProjectProbe />
        </ProjectProvider>,
      );
    });
  });

  afterEach(() => {
    act(() => root.unmount());
    container.remove();
    jest.restoreAllMocks();
  });

  test('creates and edits an anonymous action in canonical project state', () => {
    click(container.querySelector('[data-testid="add-rest-action"]'));

    let doc = currentDocument(container);
    expect(doc.project.actions).toHaveLength(1);
    expect(doc.project.actions[0]).toMatchObject({
      id: 'action_rest_1',
      method: 'GET',
      authenticationType: 'anonymous',
    });
    expect(doc.project.actions[0]).not.toHaveProperty('responseMapping');
    expect(container.querySelector('[data-testid="dirty-probe"]')?.textContent)
      .toBe('true');

    setControlValue(
      container.querySelector<HTMLSelectElement>('[data-testid="action-authentication"]'),
      'bearerToken',
    );
    expect(currentDocument(container).project.actions[0].authenticationType)
      .toBe('anonymous');

    setControlValue(
      container.querySelector<HTMLInputElement>('[data-testid="action-name"]'),
      'Create inventory item',
    );
    setControlValue(
      container.querySelector<HTMLSelectElement>('[data-testid="action-method"]'),
      'POST',
    );
    setControlValue(
      container.querySelector<HTMLInputElement>('[data-testid="action-url"]'),
      'https://api.example.com/items',
    );
    setControlValue(
      container.querySelector<HTMLTextAreaElement>('[data-testid="action-body"]'),
      '{"name":"{{components.nameInput.value}}"}',
    );

    click(container.querySelector('button[aria-label="Add query parameters row"]'));
    setControlValue(
      container.querySelector<HTMLInputElement>('input[aria-label="Query parameters key"]'),
      'limit',
    );
    setControlValue(
      container.querySelector<HTMLInputElement>('input[aria-label="Query parameters value"]'),
      '25',
    );

    doc = currentDocument(container);
    expect(doc.project.actions[0]).toMatchObject({
      name: 'Create inventory item',
      method: 'POST',
      url: 'https://api.example.com/items',
      queryParameters: { limit: '25' },
      bodyTemplate: '{"name":"{{components.nameInput.value}}"}',
      authenticationType: 'anonymous',
    });
  });

  test('keeps invalid drafts local until they are corrected', () => {
    click(container.querySelector('[data-testid="add-rest-action"]'));

    const addButton = container.querySelector<HTMLButtonElement>(
      '[data-testid="add-rest-action"]',
    );
    expect(addButton?.disabled).toBe(true);

    const nameInput = container.querySelector<HTMLInputElement>(
      '[data-testid="action-name"]',
    );
    setControlValue(nameInput, '');
    expect(currentDocument(container).project.actions[0].name)
      .toBe('New REST Action');
    expect(doneButton(container).disabled).toBe(true);

    setControlValue(nameInput, 'Valid action name');
    expect(doneButton(container).disabled).toBe(false);

    const urlInput = container.querySelector<HTMLInputElement>(
      '[data-testid="action-url"]',
    );
    setControlValue(urlInput, 'https://');
    expect(currentDocument(container).project.actions[0].url)
      .toBe('https://api.example.com');
    expect(doneButton(container).disabled).toBe(true);

    setControlValue(urlInput, 'https://api.example.com/items');
    expect(doneButton(container).disabled).toBe(false);

    click(container.querySelector(
      'button[aria-label="Add query parameters row"]',
    ));
    expect(currentDocument(container).project.actions[0].queryParameters)
      .toEqual({});
    expect(doneButton(container).disabled).toBe(true);

    setControlValue(
      container.querySelector<HTMLInputElement>(
        'input[aria-label="Query parameters key"]',
      ),
      'limit',
    );
    expect(currentDocument(container).project.actions[0].queryParameters)
      .toEqual({ limit: '' });
    expect(doneButton(container).disabled).toBe(false);
  });

  test('duplicates an action with a fresh ID and opens the copy for editing', () => {
    click(container.querySelector('[data-testid="add-rest-action"]'));
    setControlValue(
      container.querySelector<HTMLInputElement>('[data-testid="action-name"]'),
      'Fetch inventory',
    );

    click(doneButton(container));
    click(container.querySelector(
      'button[aria-label="Duplicate REST action Fetch inventory"]',
    ));

    const doc = currentDocument(container);
    expect(doc.project.actions.map((action) => action.id)).toEqual([
      'action_rest_1',
      'action_rest_2',
    ]);
    expect(doc.project.actions[1].name).toBe('Fetch inventory Copy');
    expect(container.querySelector(
      '[data-testid="rest-action-editor-action_rest_2"]',
    )).not.toBeNull();
  });

  test('requires confirmation and removes only the selected action', () => {
    click(container.querySelector('[data-testid="add-rest-action"]'));
    click(doneButton(container));
    const confirm = jest.spyOn(window, 'confirm')
      .mockReturnValueOnce(false)
      .mockReturnValueOnce(true);

    const deleteButton = () => container.querySelector(
      'button[aria-label="Delete REST action New REST Action"]',
    );

    click(deleteButton());
    expect(currentDocument(container).project.actions).toHaveLength(1);

    click(deleteButton());
    expect(currentDocument(container).project.actions).toHaveLength(0);
    expect(confirm).toHaveBeenCalledWith(
      'Delete REST action "New REST Action"? This cannot be undone.',
    );
  });

  test('authors variables and onSuccess response bindings in canonical state', () => {
    act(() => {
      root.render(<ProjectProvider><ProjectSeed /><ActionInspector /><ProjectProbe /></ProjectProvider>);
    });

    click(container.querySelector('[data-testid="add-variable"]'));
    setControlValue(container.querySelector<HTMLInputElement>('input[aria-label="Variable name"]'), 'capturedId');
    setControlValue(container.querySelector<HTMLSelectElement>('select[aria-label="Variable type"]'), 'number');
    setControlValue(container.querySelector<HTMLTextAreaElement>('textarea[aria-label="Variable default value"]'), '7');
    click(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Save variable') ?? null);
    expect(currentDocument(container).project.variables.capturedId).toEqual({ type: 'number', defaultValue: 7 });

    click(container.querySelector('[data-testid="add-variable"]'));
    setControlValue(container.querySelector<HTMLInputElement>('input[aria-label="Variable name"]'), 'enabled');
    setControlValue(container.querySelector<HTMLSelectElement>('select[aria-label="Variable type"]'), 'boolean');
    const booleanDefault = container.querySelector<HTMLSelectElement>('select[aria-label="Variable default value"]');
    expect(Array.from(booleanDefault?.options ?? []).map((option) => option.value)).toEqual(['', 'true', 'false']);
    setControlValue(booleanDefault, 'true');
    click(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Save variable') ?? null);
    expect(currentDocument(container).project.variables.enabled).toEqual({ type: 'boolean', defaultValue: true });

    click(container.querySelector('[data-testid="add-response-binding"]'));
    let binding = currentDocument(container).project.bindings[0];
    expect(binding).toMatchObject({ source: 'actions.action_lookup.response.body', target: 'components.result.value', trigger: 'onSuccess' });
    setControlValue(container.querySelector<HTMLInputElement>('input[aria-label="Binding response path"]'), 'body.id');
    setControlValue(container.querySelector<HTMLSelectElement>('select[aria-label="Binding target"]'), 'variables.capturedId');
    binding = currentDocument(container).project.bindings[0];
    expect(binding.source).toBe('actions.action_lookup.response.body.id');
    expect(binding.target).toBe('variables.capturedId');
    expect(binding.trigger).toBe('onSuccess');
  });

  test('warns about references when deleting a variable', () => {
    act(() => {
      root.render(<ProjectProvider><ProjectSeed /><ActionInspector /><ProjectProbe /></ProjectProvider>);
    });
    click(container.querySelector('[data-testid="add-variable"]'));
    setControlValue(container.querySelector<HTMLInputElement>('input[aria-label="Variable name"]'), 'capturedId');
    click(Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'Save variable') ?? null);
    click(container.querySelector('[data-testid="add-response-binding"]'));
    setControlValue(container.querySelector<HTMLSelectElement>('select[aria-label="Binding target"]'), 'variables.capturedId');
    const confirm = jest.spyOn(window, 'confirm').mockReturnValue(false);
    click(container.querySelector('button[aria-label="Delete variable capturedId"]'));
    expect(confirm).toHaveBeenCalledWith(expect.stringContaining('binding "binding_response_1"'));
    expect(currentDocument(container).project.variables.capturedId).toBeDefined();
  });

  test('does not offer legacy onClick for new bindings and resets after deletion', () => {
    act(() => {
      root.render(<ProjectProvider><ProjectSeed /><ActionInspector /><ProjectProbe /></ProjectProvider>);
    });
    click(container.querySelector('[data-testid="add-response-binding"]'));
    const trigger = container.querySelector<HTMLSelectElement>('select[aria-label="Binding trigger"]');
    expect(Array.from(trigger?.options ?? []).map((option) => option.value)).toEqual(['onSuccess']);
    const confirm = jest.spyOn(window, 'confirm').mockReturnValue(true);
    click(container.querySelector('button[aria-label="Delete binding binding_response_1"]'));
    expect(currentDocument(container).project.bindings).toEqual([]);
    expect(container.querySelector<HTMLButtonElement>('[data-testid="add-response-binding"]')?.disabled).toBe(false);
    expect(confirm).toHaveBeenCalled();
  });

  test('preserves an existing legacy onClick binding and allows one-way migration', () => {
    act(() => {
      root.render(<ProjectProvider><ProjectSeed legacyBinding /><ActionInspector /><ProjectProbe /></ProjectProvider>);
    });
    click(container.querySelector('button[aria-label="Edit binding binding_legacy"]'));
    const trigger = container.querySelector<HTMLSelectElement>('select[aria-label="Binding trigger"]');
    expect(Array.from(trigger?.options ?? []).map((option) => option.value)).toEqual(['onSuccess', 'onClick']);
    expect(currentDocument(container).project.bindings[0].trigger).toBe('onClick');
    setControlValue(trigger, 'onSuccess');
    expect(currentDocument(container).project.bindings[0].trigger).toBe('onSuccess');
    expect(Array.from(container.querySelectorAll<HTMLSelectElement>('select[aria-label="Binding trigger"] option')).map((option) => option.value)).toEqual(['onSuccess']);
  });
});
