import * as React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

// MarkdownRenderer pulls react-markdown (ESM, not transformed from node_modules
// by this package's jest config). The editor's own concern is wiring the preview,
// not markdown rendering, so isolate it with a lightweight stand-in.
jest.mock('../../blog/MarkdownRenderer', () => ({
  MarkdownRenderer: ({ content }: { content: string }) =>
    require('react').createElement('div', { 'data-testid': 'md-preview' }, content),
}));

import { DocumentEditor } from '../DocumentEditor';
import type { DocSection } from '../types';

function baseSections(): DocSection[] {
  return [
    { key: 'blog', label: 'Blog post', kind: 'markdown', value: 'hello world' },
    { key: 'linkedin', label: 'LinkedIn', kind: 'text', value: 'a post' },
    { key: 'seo', label: 'SEO', kind: 'structured', value: { meta_description: 'short', primary_keyword: 'kw' } },
    { key: 'sources', label: 'Sources', kind: 'list', value: ['https://a.com'] },
  ];
}

/** Controlled harness that mirrors how a consuming app wires the editor. */
function Harness({
  onSave,
  autosaveMs,
  readOnly,
}: {
  onSave?: (s: DocSection[]) => void | Promise<void>;
  autosaveMs?: number;
  readOnly?: boolean;
}) {
  const [sections, setSections] = React.useState<DocSection[]>(baseSections());
  return (
    <DocumentEditor
      sections={sections}
      readOnly={readOnly}
      autosaveMs={autosaveMs}
      onSave={onSave}
      header={<h2>My draft</h2>}
      onChange={(key, value) =>
        setSections((prev) => prev.map((s) => (s.key === key ? { ...s, value } : s)))
      }
    />
  );
}

describe('DocumentEditor', () => {
  it('renders a header slot and every section label', () => {
    render(<Harness />);
    expect(screen.getByText('My draft')).toBeInTheDocument();
    for (const label of ['Blog post', 'LinkedIn', 'SEO', 'Sources']) {
      expect(screen.getByRole('heading', { name: label })).toBeInTheDocument();
    }
  });

  it('markdown section shows a word count and a live preview alongside the editor', () => {
    render(<Harness />);
    expect(screen.getByText('2 words')).toBeInTheDocument();
    // split view: an editable textarea AND the rendered preview text
    expect(screen.getByLabelText('Blog post markdown')).toBeInTheDocument();
    const previews = screen.getAllByText('hello world');
    expect(previews.length).toBeGreaterThanOrEqual(1);
  });

  it('emits per-section edits through onChange (controlled)', () => {
    render(<Harness />);
    const textarea = screen.getByLabelText('LinkedIn') as HTMLTextAreaElement;
    fireEvent.change(textarea, { target: { value: 'edited post' } });
    expect((screen.getByLabelText('LinkedIn') as HTMLTextAreaElement).value).toBe('edited post');
  });

  it('structured section renders a field input and an SEO char meter on meta_description', () => {
    render(<Harness />);
    const metaInput = screen.getByLabelText('meta description') as HTMLInputElement;
    expect(metaInput.value).toBe('short');
    // the meta meter surfaces the aim band
    expect(screen.getByText(/aim 150/)).toBeInTheDocument();
    fireEvent.change(metaInput, { target: { value: 'new description' } });
    expect((screen.getByLabelText('meta description') as HTMLInputElement).value).toBe('new description');
  });

  it('list section adds, edits and removes items', () => {
    render(<Harness />);
    expect(screen.getByLabelText('Sources item 1')).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: 'Add' }));
    const item2 = screen.getByLabelText('Sources item 2') as HTMLInputElement;
    fireEvent.change(item2, { target: { value: 'https://b.com' } });
    expect((screen.getByLabelText('Sources item 2') as HTMLInputElement).value).toBe('https://b.com');
    fireEvent.click(screen.getByRole('button', { name: 'Remove item 1' }));
    expect(screen.queryByLabelText('Sources item 2')).not.toBeInTheDocument();
    expect((screen.getByLabelText('Sources item 1') as HTMLInputElement).value).toBe('https://b.com');
  });

  it('debounces onSave after an edit and shows a Saved status', async () => {
    const onSave = jest.fn().mockResolvedValue(undefined);
    render(<Harness onSave={onSave} autosaveMs={20} />);
    // no autosave before any edit
    expect(onSave).not.toHaveBeenCalled();
    fireEvent.change(screen.getByLabelText('LinkedIn'), { target: { value: 'changed' } });
    await waitFor(() => expect(onSave).toHaveBeenCalledTimes(1));
    const saved = onSave.mock.calls[0][0] as DocSection[];
    expect(saved.find((s) => s.key === 'linkedin')!.value).toBe('changed');
    expect(await screen.findByText('Saved')).toBeInTheDocument();
  });

  it('surfaces a Save failed status when onSave rejects', async () => {
    const onSave = jest.fn().mockRejectedValue(new Error('nope'));
    render(<Harness onSave={onSave} autosaveMs={20} />);
    fireEvent.change(screen.getByLabelText('LinkedIn'), { target: { value: 'boom' } });
    expect(await screen.findByText('Save failed')).toBeInTheDocument();
  });

  it('readOnly renders previews only (no editable inputs) and skips autosave', async () => {
    const onSave = jest.fn();
    render(<Harness onSave={onSave} autosaveMs={10} readOnly />);
    expect(screen.queryByLabelText('Blog post markdown')).not.toBeInTheDocument();
    expect(screen.queryByRole('textbox')).not.toBeInTheDocument();
    // markdown still renders as a preview
    expect(screen.getAllByText('hello world').length).toBeGreaterThanOrEqual(1);
    await new Promise((r) => setTimeout(r, 40));
    expect(onSave).not.toHaveBeenCalled();
  });
});
