import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Principal } from '@platform-modules/auth';
import {
  MenuAuthorizationError,
  MenuNotFoundError,
  MenuUrlSchemeError,
  MenuValidationError,
  type MenuItemRow,
} from '@platform-modules/menus';
import { getById } from '@platform-modules/content';
import type { Querier } from '@platform-modules/db';
import {
  DEFAULT_PRIMARY,
  mapMenuError,
  menusActorFromPrincipal,
  resolveMenuHref,
} from './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);

function urlItem(url: string): MenuItemRow {
  return {
    id: 'item-1',
    menuId: 'menu-1',
    parentId: null,
    position: 0,
    depth: 0,
    label: 'Link',
    targetKind: 'url',
    url,
    targetEntityType: null,
    targetEntityId: null,
    openInNew: false,
    createdAtMs: 0,
    updatedAtMs: 0,
  };
}

function entityItem(entityId: string, entityType = 'content'): MenuItemRow {
  return {
    id: 'item-2',
    menuId: 'menu-1',
    parentId: null,
    position: 1,
    depth: 0,
    label: 'Post',
    targetKind: 'entity',
    url: null,
    targetEntityType: entityType,
    targetEntityId: entityId,
    openInNew: false,
    createdAtMs: 0,
    updatedAtMs: 0,
  };
}

describe('menus host lib', () => {
  beforeEach(() => {
    mockedGetById.mockReset();
  });

  it('menusActorFromPrincipal maps admin and editor capabilities', () => {
    const admin = menusActorFromPrincipal({
      userId: 'a1',
      roles: ['admin'],
      sessionId: 's',
    } satisfies Principal);
    expect(admin).toEqual({ id: 'a1', canManageMenus: true });

    const editor = menusActorFromPrincipal({
      userId: 'e1',
      roles: ['editor'],
      sessionId: 's',
    } satisfies Principal);
    expect(editor).toEqual({ id: 'e1', canManageMenus: false });
  });

  it('mapMenuError maps structural menu errors to status codes', async () => {
    expect(mapMenuError(new MenuAuthorizationError('u', 'manageMenus')).status).toBe(403);
    expect(mapMenuError(new MenuNotFoundError('menu', 'm1')).status).toBe(404);
    expect(mapMenuError(new MenuUrlSchemeError('javascript:x')).status).toBe(400);
    expect(mapMenuError(new MenuValidationError('key', 'bad')).status).toBe(400);
    expect(mapMenuError(new Error('boom')).status).toBe(500);
  });

  it('DEFAULT_PRIMARY is a non-empty fallback nav', () => {
    expect(DEFAULT_PRIMARY.length).toBeGreaterThan(0);
    expect(DEFAULT_PRIMARY).toEqual([
      { label: 'Modules', href: '/#modules' },
      { label: 'Blueprint', href: '/blueprint/cms' },
      { label: 'UI Kit', href: '/ui-kit' },
    ]);
  });

  it('resolveMenuHref allows http(s) and path-relative urls', async () => {
    const ctx = { db: {} as Querier };
    expect(await resolveMenuHref(urlItem('https://example.com/x'), ctx)).toBe('https://example.com/x');
    expect(await resolveMenuHref(urlItem('/about'), ctx)).toBe('/about');
  });

  it('resolveMenuHref rejects dangerous url schemes', async () => {
    const ctx = { db: {} as Querier };
    expect(await resolveMenuHref(urlItem('javascript:alert(1)'), ctx)).toBeNull();
    expect(await resolveMenuHref(urlItem('data:text/html,x'), ctx)).toBeNull();
    expect(await resolveMenuHref(urlItem('//evil.test/phish'), ctx)).toBeNull();
  });

  it('resolveMenuHref resolves published public posts through getById viewer:null floor', async () => {
    const entryId = '11111111-1111-4111-8111-111111111111';
    const db = {} as Querier;
    mockedGetById.mockResolvedValueOnce({
      id: entryId,
      slug: 'public-post',
      type: 'post',
      status: 'published',
      visibility: 'public',
    } as never);
    const href = await resolveMenuHref(entityItem(entryId), { db });
    expect(href).toBe('/post/public-post');
    expect(mockedGetById).toHaveBeenCalledWith(db, entryId, null);
  });

  it('resolveMenuHref skips draft/private/trashed entries (no slug leak)', async () => {
    const db = {} as Querier;
    mockedGetById.mockResolvedValueOnce(null);
    const href = await resolveMenuHref(
      entityItem('22222222-2222-4222-8222-222222222222'),
      { db },
    );
    expect(href).toBeNull();
  });

  it('resolveMenuHref skips non-post entity types and unknown targets', async () => {
    const db = {} as Querier;
    mockedGetById.mockResolvedValueOnce({
      id: '33333333-3333-4333-8333-333333333333',
      slug: 'page-slug',
      type: 'page',
      status: 'published',
      visibility: 'public',
    } as never);
    expect(await resolveMenuHref(entityItem('33333333-3333-4333-8333-333333333333'), { db })).toBeNull();

    const unknown = { ...entityItem('x'), targetKind: 'bogus' as 'url' };
    expect(await resolveMenuHref(unknown, { db })).toBeNull();
  });
});
