import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import { PageProvider, usePages, uniqueSlug, nextPageId } from './PageContext';

/* PageContext only needs `useEditor` from @craftjs/core (for query.serialize /
   actions.deserialize during page switches) — mock just that so PageProvider
   can mount without a real <Editor> tree, following the DOM-harness pattern
   used in src/ui/AssetPicker.test.tsx (no @testing-library/react in this repo). */
vi.mock('@craftjs/core', () => ({
  useEditor: () => ({
    query: { serialize: () => '{}' },
    actions: { deserialize: vi.fn() },
  }),
}));

/* addPage mints ids via nextPageId() (timestamp + monotonic counter, M-3),
   so same-millisecond calls no longer collide on id. Date.now() is still
   pinned/advanced here for determinism across the slug-dedupe assertions
   below, independent of wall-clock timing. */
let dateNowSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
  let counter = 1_700_000_000_000;
  dateNowSpy = vi.spyOn(Date, 'now').mockImplementation(() => counter++);
});
afterEach(() => {
  dateNowSpy.mockRestore();
});

let container: HTMLDivElement;
let root: Root;

function render(ui: React.ReactElement) {
  container = document.createElement('div');
  document.body.appendChild(container);
  act(() => {
    root = createRoot(container);
    root.render(ui);
  });
}

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

describe('nextPageId (M-3: no same-millisecond id collision)', () => {
  test('two calls yield distinct ids even when Date.now() is pinned to a constant', () => {
    const spy = vi.spyOn(Date, 'now').mockReturnValue(1_700_000_000_000);
    try {
      const id1 = nextPageId();
      const id2 = nextPageId();
      expect(id1).not.toBe(id2);
    } finally {
      spy.mockRestore();
    }
  });

  test('ids are prefixed with "page_"', () => {
    expect(nextPageId()).toMatch(/^page_/);
  });
});

describe('uniqueSlug', () => {
  test('returns base unchanged when no collision', () => {
    expect(uniqueSlug('about', ['index', 'contact'])).toBe('about');
  });

  test('appends -2 on first collision, -3 on the next, etc', () => {
    expect(uniqueSlug('about', ['about'])).toBe('about-2');
    expect(uniqueSlug('about', ['about', 'about-2'])).toBe('about-3');
  });
});

describe('PageProvider slug dedupe', () => {
  test('addPage: two pages with the same name yield distinct slugs', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };

    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => {
      ctx!.addPage('About', '');
    });
    act(() => {
      ctx!.addPage('About', '');
    });

    const slugs = ctx!.pages.map((p) => p.slug);
    expect(slugs).toEqual(['index', 'about', 'about-2']);
    expect(new Set(slugs).size).toBe(slugs.length);

    unmount();
  });

  test('renamePage: renaming to an already-used slug is deduped', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };

    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => {
      ctx!.addPage('About', 'about');
    });
    act(() => {
      ctx!.addPage('Contact', 'contact');
    });

    const contactPage = ctx!.pages.find((p) => p.name === 'Contact')!;
    act(() => {
      ctx!.renamePage(contactPage.id, 'Contact', 'about');
    });

    const renamed = ctx!.pages.find((p) => p.id === contactPage.id)!;
    expect(renamed.slug).toBe('about-2');

    const slugs = ctx!.pages.map((p) => p.slug);
    expect(new Set(slugs).size).toBe(slugs.length);

    unmount();
  });

  test('M-4: a punctuation-only page name ("!!!") falls back to slug "page", never empty string', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };

    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => {
      ctx!.addPage('!!!', '');
    });

    const added = ctx!.pages.find((p) => p.name === '!!!')!;
    expect(added.slug).toBe('page');
    expect(added.slug).not.toBe('');

    // A second punctuation-only-named page dedupes to 'page-2', not ''.
    act(() => {
      ctx!.addPage('???', '');
    });
    const second = ctx!.pages.find((p) => p.name === '???')!;
    expect(second.slug).toBe('page-2');
    expect(second.slug).not.toBe('');

    unmount();
  });

  test('landing page slug always stays "index" even if renamed to collide', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };

    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    const landingId = ctx!.pages[0].id;
    act(() => {
      ctx!.renamePage(landingId, 'Whatever', 'whatever');
    });

    expect(ctx!.pages[0].slug).toBe('index');

    unmount();
  });
});
