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

/**
 * PKG-I: page duplicate / reorder / set-landing.
 *
 * The landing-page invariant is: `pages[0]` is the landing page, slug
 * LOCKED to `'index'`; every other page gets a real, unique slug. This
 * suite covers:
 *  - `applyLandingInvariant` as a pure function (unit tests, no provider).
 *  - `movePage`/`setLandingPage` re-establishing the invariant after
 *    reordering, via `PageProvider`.
 *  - `duplicatePage` inserting a copy right after the source with a copied
 *    craftState + seo and a unique slug, and switching the canvas to it.
 */

function makePage(overrides: Partial<PageData> & { id: string }): PageData {
  return { name: overrides.id, slug: overrides.id, craftState: null, ...overrides };
}

describe('applyLandingInvariant (pure)', () => {
  test('page at index 0 gets slug "index" even if it held a different slug', () => {
    const pages = [
      makePage({ id: 'a', name: 'About', slug: 'about' }),
      makePage({ id: 'b', name: 'Home', slug: 'index' }),
    ];
    const result = applyLandingInvariant(pages);
    expect(result[0].slug).toBe('index');
    expect(result[0].id).toBe('a');
  });

  test('demotes the old landing page (now at index > 0) to a unique real slug', () => {
    const pages = [
      makePage({ id: 'a', name: 'About', slug: 'about' }),
      makePage({ id: 'b', name: 'Home', slug: 'index' }),
    ];
    const result = applyLandingInvariant(pages);
    const demoted = result.find((p) => p.id === 'b')!;
    expect(demoted.slug).not.toBe('index');
    expect(demoted.slug).toBe('home');
  });

  test('never produces two pages with slug "index"', () => {
    const pages = [
      makePage({ id: 'a', name: 'About', slug: 'about' }),
      makePage({ id: 'b', name: 'Home', slug: 'index' }),
      makePage({ id: 'c', name: 'Contact', slug: 'contact' }),
    ];
    const result = applyLandingInvariant(pages);
    const indexSlugs = result.filter((p) => p.slug === 'index');
    expect(indexSlugs).toHaveLength(1);
    expect(indexSlugs[0].id).toBe('a');
  });

  test('demoted page slug is deduped against a colliding existing slug elsewhere in the array', () => {
    const pages = [
      makePage({ id: 'a', name: 'About', slug: 'about' }),
      makePage({ id: 'b', name: 'Home', slug: 'index' }), // demoted page, named "Home" -> slugifies to "home"
      makePage({ id: 'c', name: 'HomePage', slug: 'home' }), // unrelated page already using slug "home"
    ];
    const result = applyLandingInvariant(pages);
    expect(result[0].slug).toBe('index');
    const demoted = result.find((p) => p.id === 'b')!;
    expect(demoted.slug).toBe('home-2');
    const untouched = result.find((p) => p.id === 'c')!;
    expect(untouched.slug).toBe('home');

    const slugs = result.map((p) => p.slug);
    expect(new Set(slugs).size).toBe(slugs.length);
  });

  test('non-landing pages that already have a real slug are left untouched', () => {
    const pages = [
      makePage({ id: 'a', name: 'Home', slug: 'index' }),
      makePage({ id: 'b', name: 'About', slug: 'about' }),
      makePage({ id: 'c', name: 'Contact', slug: 'contact' }),
    ];
    const result = applyLandingInvariant(pages);
    expect(result[1]).toEqual(pages[1]);
    expect(result[2]).toEqual(pages[2]);
  });

  test('empty array is a no-op', () => {
    expect(applyLandingInvariant([])).toEqual([]);
  });
});

/* ---------- PageProvider-mounted coverage ---------- */

let serializeReturn = '{}';
const deserializeMock = vi.fn();

vi.mock('@craftjs/core', () => ({
  useEditor: () => ({
    query: { serialize: () => serializeReturn },
    actions: { deserialize: deserializeMock },
  }),
}));

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();
}

async function flushTimers() {
  await act(async () => {
    await new Promise((resolve) => setTimeout(resolve, 10));
  });
}

beforeEach(() => {
  serializeReturn = '{}';
  deserializeMock.mockClear();
});

describe('PageContext.movePage', () => {
  test('moves a page up, swapping with its neighbor', () => {
    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'));
    // pages: [Home(index0), About, Contact]
    const contactId = ctx!.pages[2].id;

    act(() => ctx!.movePage(contactId, 'up'));

    expect(ctx!.pages.map((p) => p.name)).toEqual(['Home', 'Contact', 'About']);
    // Landing invariant still holds -- Home untouched at index 0.
    expect(ctx!.pages[0].slug).toBe('index');

    unmount();
  });

  test('is a no-op at the top boundary', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    const homeId = ctx!.pages[0].id;
    const before = ctx!.pages.map((p) => p.id);

    act(() => ctx!.movePage(homeId, 'up'));

    expect(ctx!.pages.map((p) => p.id)).toEqual(before);

    unmount();
  });

  test('is a no-op at the bottom boundary', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    const aboutId = ctx!.pages[1].id;
    const before = ctx!.pages.map((p) => p.id);

    act(() => ctx!.movePage(aboutId, 'down'));

    expect(ctx!.pages.map((p) => p.id)).toEqual(before);

    unmount();
  });

  test('moving a non-landing page into index 0 promotes it and demotes the old landing page to a real slug', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    // pages: [Home(index0, slug index), About]
    const aboutId = ctx!.pages[1].id;
    const homeId = ctx!.pages[0].id;

    act(() => ctx!.movePage(aboutId, 'up'));
    // pages: [About, Home]

    expect(ctx!.pages.map((p) => p.id)).toEqual([aboutId, homeId]);
    expect(ctx!.pages[0].slug).toBe('index'); // About is now the landing page
    const demotedHome = ctx!.pages.find((p) => p.id === homeId)!;
    expect(demotedHome.slug).not.toBe('index');
    expect(demotedHome.slug).toBe('home');

    // Exactly one 'index' slug, always at index 0.
    const indexPages = ctx!.pages.filter((p) => p.slug === 'index');
    expect(indexPages).toHaveLength(1);
    expect(ctx!.pages.indexOf(indexPages[0])).toBe(0);

    // movePage does not touch the canvas.
    expect(deserializeMock).not.toHaveBeenCalled();

    unmount();
  });

  test('moving the current landing page down demotes it and promotes its neighbor', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    // pages: [Home(index0, slug index), About]
    const homeId = ctx!.pages[0].id;
    const aboutId = ctx!.pages[1].id;

    act(() => ctx!.movePage(homeId, 'down'));
    // pages: [About, Home]

    expect(ctx!.pages.map((p) => p.id)).toEqual([aboutId, homeId]);

    // Order changed and the landing invariant re-established: index 0
    // (now About) gets slug 'index'; the moved page (now at index 1, Home)
    // gets a real, non-'index' unique slug.
    expect(ctx!.pages[0].id).toBe(aboutId);
    expect(ctx!.pages[0].slug).toBe('index');
    const demotedHome = ctx!.pages.find((p) => p.id === homeId)!;
    expect(demotedHome.slug).not.toBe('index');
    expect(demotedHome.slug).toBe('home');

    // Exactly one 'index' slug.
    const indexPages = ctx!.pages.filter((p) => p.slug === 'index');
    expect(indexPages).toHaveLength(1);

    unmount();
  });
});

describe('PageContext.setLandingPage', () => {
  test('promotes an arbitrary page to index 0 and demotes the old landing page', () => {
    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 homeId = ctx!.pages[0].id;
    const contactId = ctx!.pages[2].id;

    act(() => ctx!.setLandingPage(contactId));

    expect(ctx!.pages[0].id).toBe(contactId);
    expect(ctx!.pages[0].slug).toBe('index');
    const demotedHome = ctx!.pages.find((p) => p.id === homeId)!;
    expect(demotedHome.slug).toBe('home');

    const indexPages = ctx!.pages.filter((p) => p.slug === 'index');
    expect(indexPages).toHaveLength(1);

    // setLandingPage does not touch the canvas.
    expect(deserializeMock).not.toHaveBeenCalled();

    unmount();
  });

  test('is a no-op when the page is already the landing page', () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    const homeId = ctx!.pages[0].id;
    const before = ctx!.pages.map((p) => ({ ...p }));

    act(() => ctx!.setLandingPage(homeId));

    expect(ctx!.pages).toEqual(before);

    unmount();
  });
});

describe('PageContext.duplicatePage', () => {
  test('inserts a copy immediately after the source with a copied craftState, seo, and a unique slug', async () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    await flushTimers();
    act(() => ctx!.addPage('Contact', 'contact'));
    await flushTimers();
    // pages: [Home, About, Contact]; Contact is currently active.

    const aboutId = ctx!.pages[1].id;
    act(() => ctx!.updatePageSeo(aboutId, { metaTitle: 'About Us' }));

    act(() => ctx!.duplicatePage(aboutId));
    await flushTimers();

    const names = ctx!.pages.map((p) => p.name);
    expect(names).toEqual(['Home', 'About', 'About copy', 'Contact']);

    const copy = ctx!.pages[2];
    expect(copy.name).toBe('About copy');
    expect(copy.slug).toBe('about-copy');
    expect(copy.seo).toEqual({ metaTitle: 'About Us' });

    // Every slug in the array is unique.
    const slugs = ctx!.pages.map((p) => p.slug);
    expect(new Set(slugs).size).toBe(slugs.length);

    // Landing invariant untouched -- copy is never index 0.
    expect(ctx!.pages[0].slug).toBe('index');

    unmount();
  });

  test('duplicating the ACTIVE page saves the live canvas into the copy (and the original)', async () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    // Home is active by default. Simulate the user having made live edits.
    serializeReturn = '{"ROOT":{"live":"edit"}}';

    act(() => ctx!.duplicatePage(ctx!.pages[0].id));
    await flushTimers();

    const copy = ctx!.pages[1];
    expect(copy.name).toBe('Home copy');
    expect(copy.craftState).toBe('{"ROOT":{"live":"edit"}}');

    // Original page's stored state was also refreshed to the live canvas.
    expect(ctx!.pages[0].craftState).toBe('{"ROOT":{"live":"edit"}}');

    // The canvas switched to the new copy.
    expect(ctx!.activePageId).toBe(copy.id);

    unmount();
  });

  test('duplicating a non-active page copies its already-stored craftState (no live-canvas read)', async () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    await flushTimers();
    // Home is now inactive, stored with whatever it serialized to on switch.
    const homeId = ctx!.pages[0].id;
    const homeCraftState = ctx!.pages[0].craftState;

    // Switch the live serialize() return to something else, to prove
    // duplicating an inactive page does NOT read the live canvas.
    serializeReturn = '{"ROOT":{"unrelated":"currently-active-page-content"}}';

    act(() => ctx!.duplicatePage(homeId));
    await flushTimers();

    const copy = ctx!.pages.find((p) => p.name === 'Home copy')!;
    expect(copy.craftState).toBe(homeCraftState);

    unmount();
  });

  test('duplicating a non-active page does NOT drop the outgoing active page\'s live unsaved edits (regression lock)', async () => {
    // Regression test for the Critical bug: duplicatePage(pageId) used to
    // call saveCurrentState() ONLY when pageId === the active page, yet
    // ALWAYS ended by tearing down the canvas via loadState() + switching
    // activePageId to the copy. If the duplicated page was NOT the active
    // one, the active page's live canvas edits were never serialized into
    // its slot before that teardown -- silently discarded. This asserts the
    // outgoing active page ('About') keeps its live-serialized craftState
    // after duplicating a DIFFERENT page ('Home').
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    act(() => ctx!.addPage('About', 'about'));
    await flushTimers();
    // pages: [Home, About]; About is active (addPage switches to it).
    const homeId = ctx!.pages[0].id;
    const aboutId = ctx!.pages[1].id;
    expect(ctx!.activePageId).toBe(aboutId);

    // Simulate the user having made live, unsaved edits to About (the
    // active page) that have not yet been serialized into pages[] state.
    const liveAboutEdit = '{"ROOT":{"live":"about-edit-not-yet-saved"}}';
    serializeReturn = liveAboutEdit;

    // Duplicate a DIFFERENT page (Home), not the active one (About).
    act(() => ctx!.duplicatePage(homeId));
    await flushTimers();

    // The outgoing active page's live edits must have been persisted into
    // its own slot before the canvas was torn down and switched away.
    const aboutAfter = ctx!.pages.find((p) => p.id === aboutId)!;
    expect(aboutAfter.craftState).toBe(liveAboutEdit);

    unmount();
  });

  test('switches the canvas to the new copy (deserialize called with the copy craftState)', async () => {
    let ctx: ReturnType<typeof usePages> | null = null;
    const Consumer: React.FC = () => {
      ctx = usePages();
      return null;
    };
    render(
      <PageProvider>
        <Consumer />
      </PageProvider>,
    );

    deserializeMock.mockClear();
    act(() => ctx!.duplicatePage(ctx!.pages[0].id));
    await flushTimers();

    expect(deserializeMock).toHaveBeenCalled();

    unmount();
  });
});
