// @vitest-environment happy-dom
//
// Renders the real component with react-dom/client (same style as
// FileTree.statuses.test.jsx — there is no react-testing-library here).
// DialogShell portals into document.body, so queries target the body.
import { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { PromptDialog } from './FileDialogs';

let container;

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

afterEach(() => {
  document.body.removeChild(container);
  container = null;
});

function render(props) {
  const root = createRoot(container);
  act(() => {
    root.render(
      <PromptDialog
        icon="assets/folder.svg"
        title="New Folder"
        label="Folder name"
        initialValue=""
        confirmLabel="Create"
        validate={() => null}
        onSubmit={async () => ({ ok: true })}
        onCancel={() => {}}
        {...props}
      />
    );
  });
  return root;
}

const input = () => document.body.querySelector('.cbm-prompt-input');
const confirmBtn = () =>
  [...document.body.querySelectorAll('.cbm-confirm-actions button')].find(
    (b) => b.textContent === 'Create'
  );
const errorText = () => {
  const el = document.body.querySelector('.cbm-prompt-error');
  return el.classList.contains('cbm-prompt-error--hidden') ? null : el.textContent;
};

// React caches the last value it wrote to the input and ignores an `input`
// event whose value it believes is unchanged. Going through the prototype's
// native setter updates the node behind that cache, so the event lands.
function type(value) {
  act(() => {
    const el = input();
    const setValue = Object.getOwnPropertyDescriptor(
      window.HTMLInputElement.prototype,
      'value'
    ).set;
    setValue.call(el, value);
    el.dispatchEvent(new Event('input', { bubbles: true }));
  });
}

describe('PromptDialog', () => {
  it('starts with the confirm disabled and no error scolding an untouched field', () => {
    const root = render({ validate: (v) => (v.trim() ? null : 'Enter a folder name.') });
    try {
      expect(confirmBtn().disabled).toBe(true);
      expect(errorText()).toBeNull();
    } finally {
      act(() => root.unmount());
    }
  });

  it('shows the validation error and blocks confirm once the field is touched', () => {
    const root = render({ validate: (v) => (v.includes('/') ? 'No slashes.' : null) });
    try {
      type('a/b');
      expect(errorText()).toBe('No slashes.');
      expect(confirmBtn().disabled).toBe(true);

      type('ab');
      expect(errorText()).toBeNull();
      expect(confirmBtn().disabled).toBe(false);
    } finally {
      act(() => root.unmount());
    }
  });

  it('submits the trimmed value', async () => {
    const onSubmit = vi.fn(async () => ({ ok: true }));
    const root = render({ onSubmit });
    try {
      type('  Textures  ');
      await act(async () => confirmBtn().click());
      expect(onSubmit).toHaveBeenCalledWith('Textures');
    } finally {
      act(() => root.unmount());
    }
  });

  it('keeps the dialog open with the typed value when the submit is rejected', async () => {
    const onSubmit = vi.fn(async () => ({ ok: false, error: 'Already exists.' }));
    const root = render({ onSubmit });
    try {
      type('Textures');
      await act(async () => confirmBtn().click());

      expect(errorText()).toBe('Already exists.');
      expect(input()).not.toBeNull();
      expect(input().value).toBe('Textures');

      // Editing clears a rejection that described the *previous* name.
      type('Meshes');
      expect(errorText()).toBeNull();
    } finally {
      act(() => root.unmount());
    }
  });

  it('submits on Enter', async () => {
    const onSubmit = vi.fn(async () => ({ ok: true }));
    const root = render({ onSubmit });
    try {
      type('Textures');
      await act(async () => {
        input().dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
        );
      });
      expect(onSubmit).toHaveBeenCalledWith('Textures');
    } finally {
      act(() => root.unmount());
    }
  });

  it('disables Create while a submit is in flight', async () => {
    let release;
    const gate = new Promise((r) => {
      release = r;
    });
    const onSubmit = vi.fn(() => gate);
    const root = render({ onSubmit });
    try {
      type('Textures');
      await act(async () => confirmBtn().click());

      expect(confirmBtn().disabled).toBe(true);

      await act(async () => {
        release({ ok: true });
        await gate;
      });
    } finally {
      act(() => root.unmount());
    }
  });

  it('ignores a second submit attempt (click or Enter) while one is in flight', async () => {
    let release;
    const gate = new Promise((r) => {
      release = r;
    });
    const onSubmit = vi.fn(() => gate);
    const root = render({ onSubmit });
    try {
      type('Textures');
      await act(async () => confirmBtn().click());
      expect(onSubmit).toHaveBeenCalledTimes(1);

      // A second click while busy must not re-invoke onSubmit.
      await act(async () => confirmBtn().click());
      expect(onSubmit).toHaveBeenCalledTimes(1);

      // Nor must pressing Enter.
      await act(async () => {
        input().dispatchEvent(
          new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })
        );
      });
      expect(onSubmit).toHaveBeenCalledTimes(1);

      await act(async () => {
        release({ ok: true });
        await gate;
      });
    } finally {
      act(() => root.unmount());
    }
  });

  it('recovers from a rejected onSubmit: reopens, shows an error, keeps the value, re-enables Create', async () => {
    const onSubmit = vi.fn(() => Promise.reject(new Error('Network fell over.')));
    const root = render({ onSubmit });
    try {
      type('Textures');
      await act(async () => confirmBtn().click());

      expect(input()).not.toBeNull();
      expect(input().value).toBe('Textures');
      expect(errorText()).toBe('Network fell over.');
      expect(confirmBtn().disabled).toBe(false);
    } finally {
      act(() => root.unmount());
    }
  });
});
