import { describe, test, expect, vi } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';

/* -------------------------------------------------------------------------
 * Mocked-hook tests -- pure UI-logic coverage: the domain-match guard, that
 * confirming calls the right context functions with tree-shaped arguments,
 * and that standalone mode (empty siteDomain) hides the entry point
 * entirely. The real `replaceAllPages`/`setHeader`/`setFooter`/
 * `resetToDefaults` implementations (and whether the blank trees this
 * component builds actually survive `treeToCraftState`/`sanitizeAiTree`) are
 * covered separately, WITHOUT mocks, in
 * `SiteDesignPanel.reset.integration.test.tsx` -- `vi.mock` is hoisted and
 * file-scoped, so it can't be selectively "undone" partway through one file
 * for a real-provider test.
 * ---------------------------------------------------------------------- */

const replaceAllPages = vi.fn();
const setHeader = vi.fn();
const setFooter = vi.fn();
const resetToDefaults = vi.fn();
let mockSiteDomain = 'example.com';

vi.mock('../../state/PageContext', () => ({
  usePages: () => ({ replaceAllPages, setHeader, setFooter, pages: [], siteDesign: {} }),
}));
vi.mock('../../state/SiteDesignContext', () => ({
  useSiteDesign: () => ({ design: {}, updateDesign: vi.fn(), resetToDefaults }),
  DEFAULT_SITE_DESIGN: {},
}));
vi.mock('../../state/EditorConfigContext', () => ({
  useEditorConfig: () => ({ whpConfig: mockSiteDomain ? { siteDomain: mockSiteDomain } : null, isWHP: !!mockSiteDomain }),
}));

import { SiteDesignPanel, siteResetConfirmMatches } from './SiteDesignPanel';

let container: HTMLDivElement;
let root: Root;

function render() {
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
    root.render(<SiteDesignPanel />);
  });
}

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

/** React tracks a controlled `<input>`'s value via a wrapped native setter --
 *  a plain `input.value = x` assignment doesn't go through it, so React never
 *  sees the change and skips onChange. Using the real native setter (same
 *  pattern as `MediaStylePanel.slides.test.tsx`) makes the subsequent
 *  `input` event register as a genuine value change. */
function setInputValue(input: HTMLInputElement, value: string) {
  const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')!.set!;
  setter.call(input, value);
  input.dispatchEvent(new Event('input', { bubbles: true }));
}

/**
 * Reach past React's OWN disabled-button protection to grab the `onClick`
 * function it attached to a DOM node, and call it directly.
 *
 * Verified empirically (see task-15 follow-up investigation) that neither
 * `el.disabled = false; el.click()` nor a raw `dispatchEvent(new
 * MouseEvent('click', ...))` actually invokes a React `onClick` handler once
 * React has rendered the element with `disabled` truthy: react-dom's event
 * system special-cases form controls and refuses to dispatch synthetic
 * click/similar events against its own last-rendered `disabled` prop,
 * regardless of what the live DOM property says. That's a second, redundant
 * safety net on top of the native browser behavior -- but it also means a
 * pure DOM-level "disabled bypass" can't reach `handleResetSite` at all
 * through the button, and so can't exercise the handler's OWN guard the way
 * this test needs to. Pulling `onClick` off the fiber's stashed props
 * (`__reactProps$...`, the same object React itself calls into) and
 * invoking it directly is what actually reaches `handleResetSite` -- the
 * same code path a differently-wired future trigger (a keyboard shortcut, a
 * second button, a copy-paste bug) would also go through without passing
 * back through the `disabled` gate at all.
 */
function invokeReactOnClick(el: HTMLElement): void {
  const propsKey = Object.keys(el).find((k) => k.startsWith('__reactProps$'));
  const onClick = propsKey ? (el as unknown as Record<string, { onClick?: () => void }>)[propsKey].onClick : undefined;
  if (!onClick) throw new Error('no React onClick prop found on element');
  onClick();
}

describe('siteResetConfirmMatches -- the guard predicate itself', () => {
  test('matches only an exact (trimmed) domain match', () => {
    expect(siteResetConfirmMatches('example.com', 'example.com')).toBe(true);
    expect(siteResetConfirmMatches('example.co', 'example.com')).toBe(false);
    expect(siteResetConfirmMatches('', 'example.com')).toBe(false);
    expect(siteResetConfirmMatches('  example.com  ', 'example.com')).toBe(true);
  });

  test('an empty domain never matches, even against an empty typed value -- ' +
    'this is the guard that must hold even if the (currently hidden) entry ' +
    'point were ever reachable in standalone mode', () => {
    expect(siteResetConfirmMatches('', '')).toBe(false);
    expect(siteResetConfirmMatches('   ', '')).toBe(false);
  });
});

describe('Reset Entire Site -- guard + wiring (mocked hooks)', () => {
  test('the confirm button is disabled until the domain is typed exactly', () => {
    mockSiteDomain = 'example.com';
    render();
    act(() => { (container.querySelector('[data-action="open-site-reset"]') as HTMLButtonElement).click(); });

    const confirm = container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement;
    expect(confirm.disabled).toBe(true);

    const input = container.querySelector('[data-testid="site-reset-domain"]') as HTMLInputElement;
    act(() => { setInputValue(input, 'example.co'); });
    expect((container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).disabled).toBe(true);

    act(() => { setInputValue(input, 'example.com'); });
    expect((container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).disabled).toBe(false);

    unmount();
  });

  test('confirming blanks pages, header, footer and design tokens', () => {
    mockSiteDomain = 'example.com';
    replaceAllPages.mockClear();
    setHeader.mockClear();
    setFooter.mockClear();
    resetToDefaults.mockClear();

    render();
    act(() => { (container.querySelector('[data-action="open-site-reset"]') as HTMLButtonElement).click(); });
    const input = container.querySelector('[data-testid="site-reset-domain"]') as HTMLInputElement;
    act(() => { setInputValue(input, 'example.com'); });
    act(() => { (container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement).click(); });

    expect(replaceAllPages).toHaveBeenCalledTimes(1);
    const pagesArg = replaceAllPages.mock.calls[0][0];
    expect(pagesArg).toHaveLength(1);
    expect(pagesArg[0].name).toBe('Home');
    // The replacement is a SerializedTreeNode (recursive tree), not a flat
    // craft-state entry: a real Container root with no children.
    expect(pagesArg[0].tree.type.resolvedName).toBe('Container');
    expect(pagesArg[0].tree.nodes).toEqual([]);

    expect(setHeader).toHaveBeenCalledTimes(1);
    expect(setHeader.mock.calls[0][0].type.resolvedName).toBe('Container');
    expect(setFooter).toHaveBeenCalledTimes(1);
    expect(setFooter.mock.calls[0][0].type.resolvedName).toBe('Container');
    expect(resetToDefaults).toHaveBeenCalledTimes(1);

    // The panel closes its own dialog back up after confirming.
    expect(container.querySelector('[data-action="confirm-site-reset"]')).toBeNull();
    expect(container.querySelector('[data-action="open-site-reset"]')).toBeTruthy();

    unmount();
  });

  test('the handler itself refuses a non-matching value, even when its onClick is invoked ' +
    'directly (bypassing `disabled` and React\'s own disabled-click suppression) -- proves ' +
    'the guard is load-bearing in handleResetSite, not just a UI affordance a different ' +
    'trigger path could route around', () => {
    mockSiteDomain = 'example.com';
    replaceAllPages.mockClear();
    setHeader.mockClear();
    setFooter.mockClear();
    resetToDefaults.mockClear();

    render();
    act(() => { (container.querySelector('[data-action="open-site-reset"]') as HTMLButtonElement).click(); });
    const input = container.querySelector('[data-testid="site-reset-domain"]') as HTMLInputElement;
    act(() => { setInputValue(input, 'not-the-domain'); });

    const confirm = container.querySelector('[data-action="confirm-site-reset"]') as HTMLButtonElement;
    expect(confirm.disabled).toBe(true); // sanity: the UI affordance is still doing its job too

    act(() => { invokeReactOnClick(confirm); });

    expect(replaceAllPages).not.toHaveBeenCalled();
    expect(setHeader).not.toHaveBeenCalled();
    expect(setFooter).not.toHaveBeenCalled();
    expect(resetToDefaults).not.toHaveBeenCalled();
    // The dialog is still open -- handleResetSite returned early before
    // reaching its own close-the-dialog cleanup.
    expect(container.querySelector('[data-action="confirm-site-reset"]')).toBeTruthy();

    unmount();
  });

  test('standalone mode (no WHP_CONFIG, siteDomain "") hides the entry point entirely, ' +
    'while non-standalone mode shows it -- an empty typed input must never trivially ' +
    'satisfy the guard. Asserted as a contrast within one test (not "absence" alone) ' +
    'so this fails if the whole feature -- not just the guard -- were ever removed.', () => {
    mockSiteDomain = '';
    render();
    expect(container.querySelector('[data-action="open-site-reset"]')).toBeNull();
    expect(container.querySelector('[data-testid="site-reset-domain"]')).toBeNull();
    expect(container.textContent).not.toContain('Danger zone');
    expect(container.textContent).not.toContain('Reset Entire Site');
    unmount();

    mockSiteDomain = 'example.com';
    render();
    expect(container.querySelector('[data-action="open-site-reset"]')).toBeTruthy();
    expect(container.textContent).toContain('Danger zone');
    unmount();
  });
});
