import { describe, it, expect, vi, beforeEach } from 'vitest';
import { sql } from 'drizzle-orm';
import { createPgliteClient } from '@platform-modules/db/pglite';
import {
  createMenu,
  addMenuItem,
  getMenuTree,
  menusMigrationSql,
  menusSchema,
  type MenusSchema,
} from '@platform-modules/menus';
import { getById } from '@platform-modules/content';
import type { Querier } from '@platform-modules/db';
import type { ContentSchema } from '@platform-modules/content';
import { DEFAULT_PRIMARY, resolveMenuHref } from '../lib/menus.js';

vi.mock('@platform-modules/content', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@platform-modules/content')>();
  return { ...actual, getById: vi.fn() };
});

const mockedGetById = vi.mocked(getById);

/** Mirrors the server-rendered header nav contract in `layouts/Base.astro` — nav is rendered in the theme Shell (DefaultShell/EditorialShell); resolution stays in Base.astro. */
export async function buildSiteNavLinks(
  menusDb: Querier<MenusSchema>,
  contentDb: Querier<ContentSchema>,
): Promise<{ label: string; href: string }[]> {
  const tree = await getMenuTree(menusDb, 'primary');
  if (!tree || tree.items.length === 0) return DEFAULT_PRIMARY;

  const resolved = await Promise.all(
    tree.items.map(async (item) => {
      const href = await resolveMenuHref(item, { db: contentDb });
      return href ? { label: item.label, href } : null;
    }),
  );
  const links = resolved.filter((link): link is { label: string; href: string } => link != null);
  return links.length > 0 ? links : DEFAULT_PRIMARY;
}

export function renderSiteNavHtml(links: { label: string; href: string }[]): string {
  const chunks = ['<nav class="site-nav" aria-label="Main">'];
  for (const link of links) {
    chunks.push(`<a href="${link.href}">${link.label}</a>`);
  }
  chunks.push('</nav>');
  return chunks.join('');
}

const admin = { id: 'admin', canManageMenus: true };

async function insertLegacyMenuItem(
  db: Querier<MenusSchema>,
  menuId: string,
  row: {
    id: string;
    label: string;
    position: number;
    targetKind: 'url' | 'entity';
    url?: string | null;
    targetEntityType?: string | null;
    targetEntityId?: string | null;
  },
): Promise<void> {
  await db.execute(sql`
    INSERT INTO menu_items (
      id, menu_id, parent_id, position, depth, label, target_kind, url,
      target_entity_type, target_entity_id, open_in_new, created_at_ms, updated_at_ms
    ) VALUES (
      ${row.id}, ${menuId}, NULL, ${row.position}, 0, ${row.label}, ${row.targetKind},
      ${row.url ?? null}, ${row.targetEntityType ?? null}, ${row.targetEntityId ?? null},
      false, 0, 0
    )
  `);
}

async function createMenusDb(): Promise<Querier<MenusSchema>> {
  const db = createPgliteClient({ schema: menusSchema });
  for (const statement of menusMigrationSql()
    .split(';')
    .map((s) => s.trim())
    .filter(Boolean)) {
    await db.execute(sql.raw(statement));
  }
  return db;
}

describe('Base.astro public nav', () => {
  beforeEach(() => {
    mockedGetById.mockReset();
  });

  it('falls back to DEFAULT_PRIMARY when primary menu is absent or empty', async () => {
    const menusDb = await createMenusDb();
    const contentDb = {} as Querier<ContentSchema>;
    expect(await buildSiteNavLinks(menusDb, contentDb)).toEqual(DEFAULT_PRIMARY);

    const menu = await createMenu(menusDb, admin, { key: 'primary', label: 'Primary' });
    expect(menu.key).toBe('primary');
    expect(await buildSiteNavLinks(menusDb, contentDb)).toEqual(DEFAULT_PRIMARY);
  });

  it('renders resolved primary menu links and skips null hrefs', async () => {
    const menusDb = await createMenusDb();
    const menu = await createMenu(menusDb, admin, { key: 'primary', label: 'Primary' });
    await addMenuItem(menusDb, admin, menu.id, {
      label: 'About',
      target: { kind: 'url', url: '/about' },
    });
    await insertLegacyMenuItem(menusDb, menu.id, {
      id: 'bad-scheme',
      label: 'Bad scheme',
      position: 1,
      targetKind: 'url',
      url: 'javascript:alert(1)',
    });
    const draftId = '22222222-2222-4222-8222-222222222222';
    await addMenuItem(menusDb, admin, menu.id, {
      label: 'Draft post',
      target: { kind: 'entity', entityType: 'content', entityId: draftId },
    });
    mockedGetById.mockResolvedValue(null);

    const contentDb = {} as Querier<ContentSchema>;
    const links = await buildSiteNavLinks(menusDb, contentDb);
    const html = renderSiteNavHtml(links);

    expect(links).toEqual([{ label: 'About', href: '/about' }]);
    expect(html).toContain('href="/about"');
    expect(html).not.toContain('javascript:');
    expect(html).not.toContain('draft-post');
    expect(html).not.toContain(draftId);
  });

  it('uses DEFAULT_PRIMARY when every root item resolves to null', async () => {
    const menusDb = await createMenusDb();
    const menu = await createMenu(menusDb, admin, { key: 'primary', label: 'Primary' });
    await insertLegacyMenuItem(menusDb, menu.id, {
      id: 'blocked-url',
      label: 'Blocked',
      position: 0,
      targetKind: 'url',
      url: 'data:text/html,x',
    });

    const contentDb = {} as Querier<ContentSchema>;
    expect(await buildSiteNavLinks(menusDb, contentDb)).toEqual(DEFAULT_PRIMARY);
  });

  it('renders published entity targets without leaking draft slugs', async () => {
    const menusDb = await createMenusDb();
    const menu = await createMenu(menusDb, admin, { key: 'primary', label: 'Primary' });
    const publishedId = '11111111-1111-4111-8111-111111111111';
    await addMenuItem(menusDb, admin, menu.id, {
      label: 'Public post',
      target: { kind: 'entity', entityType: 'content', entityId: publishedId },
    });
    mockedGetById.mockResolvedValueOnce({
      id: publishedId,
      slug: 'public-post',
      type: 'post',
      status: 'published',
      visibility: 'public',
    } as never);

    const contentDb = {} as Querier<ContentSchema>;
    const links = await buildSiteNavLinks(menusDb, contentDb);
    expect(links).toEqual([{ label: 'Public post', href: '/post/public-post' }]);

    const draftSlug = 'secret-draft-slug';
    mockedGetById.mockResolvedValueOnce(null);
    const draftItem = (await getMenuTree(menusDb, 'primary'))!.items[0]!;
    const draftHref = await resolveMenuHref(
      {
        ...draftItem,
        id: 'draft-item',
        label: 'Secret draft',
        targetEntityId: '33333333-3333-4333-8333-333333333333',
      },
      { db: contentDb },
    );
    expect(draftHref).toBeNull();

    const html = renderSiteNavHtml(links);
    expect(html).toContain('/post/public-post');
    expect(html).not.toContain(draftSlug);
  });
});
