import { test } from 'node:test'
import assert from 'node:assert/strict'

import { navItemVisibleTo, allowedPathsFor, isAllowedPath, firstDestinationFor } from '../src/lib/adminNav.js'

// Moderator confinement, derived from each row's `roles` (Phase 2 PR 8 —
// MODULE_SYSTEM.md §1.4). This replaced two hardcoded path lists that had
// drifted apart from each other, so the tests worth having are the ones that
// pin what a moderator may now see and reach, and the shape of the match.

// The real sidebar, trimmed to the rows that decide something here.
const NAV = [
  { items: [{ to: '/admin', label: 'Dashboard', end: true, roles: ['admin', 'editor', 'moderator'] }] },
  {
    title: 'Moderation',
    items: [
      { to: '/admin/moderation', label: 'Moderation', roles: ['admin', 'moderator'] },
      { to: '/admin/moderation/appeals', label: 'Appeals', roles: ['admin', 'moderator'] },
      { to: '/admin/shard-ops', label: 'In-Game Ops', roles: ['admin', 'moderator'] },
      { to: '/admin/houses', label: 'Houses', roles: ['admin', 'moderator'] },
    ],
  },
  {
    title: 'System',
    items: [
      { to: '/admin/users', label: 'Users', roles: ['admin'] },
      { to: '/admin/settings', label: 'Settings', roles: ['admin'] },
    ],
  },
  {
    items: [
      { to: '/admin/characters', label: 'My Characters' },
      { to: '/admin/account', label: 'Account' },
    ],
  },
]

const visibleTo = (role) =>
  NAV.flatMap((g) => g.items)
    .filter((i) => navItemVisibleTo(i, role))
    .map((i) => i.to)

test('a row with no roles is visible to every staff role', () => {
  // Self-service: staff are a superset of players, so a moderator reaching their
  // own characters is not a privilege, it is the thing every account has.
  for (const role of ['admin', 'editor', 'moderator']) {
    assert.equal(navItemVisibleTo({ to: '/admin/account' }, role), true)
  }
})

test('a role not named on the row cannot see it', () => {
  assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, 'moderator'), false)
  assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, 'admin'), true)
  // An unknown or absent role sees only the ungated rows.
  assert.equal(navItemVisibleTo({ to: '/admin/users', roles: ['admin'] }, undefined), false)
  assert.equal(navItemVisibleTo({ to: '/admin/account' }, undefined), true)
})

test('what a moderator sees is exactly the moderation section, plus self-service', () => {
  // The two additions the derivation makes over the old MOD_PATHS list are
  // Dashboard — whose roles have always named moderator, so the two lists
  // disagreed — and My Characters. Both are already permitted server-side.
  assert.deepEqual(visibleTo('moderator'), [
    '/admin',
    '/admin/moderation',
    '/admin/moderation/appeals',
    '/admin/shard-ops',
    '/admin/houses',
    '/admin/characters',
    '/admin/account',
  ])
})

test('an admin still sees everything and an editor still sees nothing extra', () => {
  assert.equal(visibleTo('admin').length, NAV.flatMap((g) => g.items).length)
  assert.deepEqual(visibleTo('editor'), ['/admin', '/admin/characters', '/admin/account'])
})

test('a row with `end` matches exactly — the dashboard is not a prefix', () => {
  // The bug this shape exists to prevent: treating `/admin` as a prefix would
  // make every path in the admin area allowed for anyone who can see Dashboard.
  const allowed = allowedPathsFor(NAV, 'moderator')
  assert.equal(isAllowedPath('/admin', allowed), true)
  assert.equal(isAllowedPath('/admin/users', allowed), false)
  assert.equal(isAllowedPath('/admin/users/12', allowed), false)
})

test('every other row covers its own sub-routes', () => {
  const allowed = allowedPathsFor(NAV, 'moderator')
  assert.equal(isAllowedPath('/admin/moderation/appeals/12', allowed), true)
  assert.equal(isAllowedPath('/admin/characters/0x4001', allowed), true)
})

test('a sibling path that merely shares a prefix is NOT covered', () => {
  const allowed = allowedPathsFor(NAV, 'moderator')
  // `/admin/houses-secret` starts with `/admin/houses` as a string; the match is
  // on path segments, so it does not start with `/admin/houses/`.
  assert.equal(isAllowedPath('/admin/houses-secret', allowed), false)
  assert.equal(isAllowedPath('/admin/houses/42', allowed), true)
})

test('Houses is reachable, which is the defect the derivation fixed', () => {
  // The redirect used to allow only /admin/moderation*, /admin/shard-ops* and
  // /admin/account, while the sidebar showed Houses — so a moderator clicking a
  // row in their own nav was bounced back to Moderation.
  const allowed = allowedPathsFor(NAV, 'moderator')
  assert.equal(isAllowedPath('/admin/houses', allowed), true)
})

test('a module row a moderator may see is reachable without core listing it', () => {
  // The reason this is derived at all: core cannot hardcode a path it has never
  // heard of, and a module row arrives with `roles` like any other.
  const withModule = [
    ...NAV,
    { title: 'Shard', items: [{ to: '/admin/uo/shard-ops', label: 'Ops', roles: ['admin', 'moderator'], moduleId: 'uo' }] },
  ]
  const allowed = allowedPathsFor(withModule, 'moderator')
  assert.equal(isAllowedPath('/admin/uo/shard-ops', allowed), true)
  assert.equal(isAllowedPath('/admin/uo/shard-ops/queue', allowed), true)
})

test('a nav that is not there does not throw', () => {
  assert.deepEqual(allowedPathsFor(null, 'moderator'), [])
  assert.equal(isAllowedPath('/admin', undefined), false)
})

// ── firstDestinationFor: where an area's index goes (Phase 3 slice 3) ────────
//
// `/player` used to be `PlayerCharacters`, a UO page. When the client half was
// extracted the portal had no index at all, and rather than pick a fixed page or
// invent a core landing screen, the index resolves to the first row this viewer
// can reach. The interesting properties are that it follows the ROLE and that it
// reads the base nav, not the merged one.

const PORTAL_NAV = [
  { to: '/account/appeals', label: 'Appeals' },
  { to: '/account', label: 'Account', end: true },
]

test('the index is the first row a viewer can actually reach', () => {
  assert.equal(firstDestinationFor(PORTAL_NAV, 'player', '/account'), '/account/appeals')
})

test('a module row registered at the front becomes the index', () => {
  // The behaviour that makes this a non-regression: with module-uo installed,
  // Characters is the first row again and a player still lands on it.
  const withModule = [{ to: '/player/uo/characters', label: 'Characters', moduleId: 'uo' }, ...PORTAL_NAV]
  assert.equal(firstDestinationFor(withModule, 'player', '/account'), '/player/uo/characters')
})

test('a row this role cannot see is skipped, not landed on', () => {
  const gated = [{ to: '/player/uo/staff', label: 'Staff', roles: ['admin'] }, ...PORTAL_NAV]
  assert.equal(firstDestinationFor(gated, 'player', '/account'), '/account/appeals')
  assert.equal(firstDestinationFor(gated, 'admin', '/account'), '/player/uo/staff')
})

test('an empty or all-gated nav falls back rather than resolving to nothing', () => {
  assert.equal(firstDestinationFor([], 'player', '/account'), '/account')
  assert.equal(firstDestinationFor(null, 'player', '/account'), '/account')
  const allGated = [{ to: '/x', label: 'X', roles: ['admin'] }]
  assert.equal(firstDestinationFor(allGated, 'player', '/account'), '/account')
})

test('it reads the grouped admin nav too, flattening in order', () => {
  // Same function for both areas, which is the point: the admin index is a
  // hardcoded Dashboard today, and if the two logged-in areas ever merge this is
  // what answers for the result.
  assert.equal(firstDestinationFor(NAV, 'moderator', '/admin/account'), '/admin')
  // An editor cannot see Dashboard's neighbours in Moderation, so they land on
  // the first row they can see wherever it is.
  assert.equal(firstDestinationFor(NAV, 'editor', '/admin/account'), '/admin')
})
