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

import { withModuleNav } from '../src/modules/nav.js'
import { registerNav, _reset } from '../src/modules/registry.js'
import { applyNavOverrides, buildPublicNav } from '../src/lib/navOverrides.js'

// The interleave of module nav rows into core's nav (MODULE_API.md §3.3, Phase 2
// PR 8). Tested against the real merge next door rather than in isolation,
// because the property that matters is a relationship between the two: a module
// row has to be indistinguishable from a core row to everything downstream, and
// the way to prove that is to run the downstream thing on it.

const PUBLIC = [
  { label: 'Home', to: '/', end: true },
  { label: 'News', to: '/site/news' },
  { label: 'About', to: '/site/about' },
]

const ADMIN = [
  { items: [{ to: '/admin', label: 'Dashboard', end: true, roles: ['admin', 'moderator'] }] },
  { title: 'Moderation', items: [{ to: '/admin/moderation', label: 'Moderation' }] },
  { title: 'System', items: [{ to: '/admin/users', label: 'Users' }, { to: '/admin/settings', label: 'Settings' }] },
  { items: [{ to: '/admin/account', label: 'Account' }] },
]

beforeEach(() => _reset())

test('with no module installed the base array is returned unchanged', () => {
  // Identity, not a copy: this is what makes the useMemo in each layout honest,
  // and what guarantees an instance with no modules renders what it renders now.
  assert.equal(withModuleNav(PUBLIC, 'public'), PUBLIC)
  assert.equal(withModuleNav(ADMIN, 'admin'), ADMIN)
})

test('a flat nav places a module row by the order it asked for', () => {
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas', order: 1 }] })
  assert.deepEqual(
    withModuleNav(PUBLIC, 'public').map((i) => i.label),
    ['Home', 'Atlas', 'News', 'About'],
  )
})

test('a flat row with no order appends rather than jumping to the front', () => {
  // The 0-default trap: `order ?? 0` would put an unordered row first, which is
  // the one place a module could take over the nav without asking for anything.
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas' }] })
  assert.deepEqual(
    withModuleNav(PUBLIC, 'public').map((i) => i.label),
    ['Home', 'News', 'About', 'Atlas'],
  )
})

test('an explicit order beats a core row that merely sits at that index', () => {
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas', order: 2 }] })
  const labels = withModuleNav(PUBLIC, 'public').map((i) => i.label)
  assert.deepEqual(labels, ['Home', 'News', 'Atlas', 'About'])
})

test('an admin row lands INSIDE the core group it names', () => {
  registerNav('uo', {
    area: 'admin',
    items: [
      { label: 'In-Game Ops', to: '/admin/uo/shard-ops', group: 'Moderation', order: 30 },
      { label: 'Shard', to: '/admin/uo/link', group: 'System', order: 0 },
    ],
  })
  const nav = withModuleNav(ADMIN, 'admin')
  assert.deepEqual(nav.map((g) => g.title), [undefined, 'Moderation', 'System', undefined])
  assert.deepEqual(nav[1].items.map((i) => i.label), ['Moderation', 'In-Game Ops'])
  // order 0 puts it above both core rows, which is the whole point of the field.
  assert.deepEqual(nav[2].items.map((i) => i.label), ['Shard', 'Users', 'Settings'])
})

test('an unknown group appends a new group instead of dropping the row', () => {
  // A typo must cost a position, never a link.
  registerNav('uo', { area: 'admin', items: [{ label: 'Atlas', to: '/admin/uo/atlas', group: 'Moderaton' }] })
  const nav = withModuleNav(ADMIN, 'admin')
  assert.equal(nav.length, ADMIN.length + 1)
  assert.deepEqual(nav.at(-1), { title: 'Moderaton', items: [{ label: 'Atlas', to: '/admin/uo/atlas', group: 'Moderaton', moduleId: 'uo' }] })
})

test('an admin row with no group gets a trailing untitled group of its own', () => {
  // NOT folded into one of core's untitled groups: those are Dashboard at the
  // top and Account at the bottom, and a module page belongs beside neither.
  registerNav('uo', { area: 'admin', items: [{ label: 'Atlas', to: '/admin/uo/atlas' }] })
  const nav = withModuleNav(ADMIN, 'admin')
  assert.equal(nav.length, ADMIN.length + 1)
  assert.equal(nav.at(-1).title, undefined)
  assert.deepEqual(nav.at(-1).items.map((i) => i.label), ['Atlas'])
  assert.deepEqual(nav[0].items.map((i) => i.label), ['Dashboard'])
  assert.deepEqual(nav[3].items.map((i) => i.label), ['Account'])
})

test('a row whose `to` collides with a core row is dropped, not rendered twice', () => {
  // `to` is the key the override layer stores under and React renders by. Two
  // rows sharing one would give an admin a single editor row that moves both.
  const warnings = []
  const warn = console.warn
  console.warn = (msg) => warnings.push(msg)
  try {
    registerNav('uo', {
      area: 'public',
      items: [{ label: 'Not News', to: '/site/news' }, { label: 'Atlas', to: '/uo/atlas' }],
    })
    const nav = withModuleNav(PUBLIC, 'public')
    assert.deepEqual(nav.map((i) => i.label), ['Home', 'News', 'About', 'Atlas'])
    assert.equal(warnings.length, 1)
    assert.match(warnings[0], /\/site\/news.*collides/)
  } finally {
    console.warn = warn
  }
})

test('two modules cannot claim the same path either', () => {
  const warn = console.warn
  console.warn = () => {}
  try {
    registerNav('aa', { area: 'public', items: [{ label: 'First', to: '/shared' }] })
    registerNav('zz', { area: 'public', items: [{ label: 'Second', to: '/shared' }] })
    const labels = withModuleNav(PUBLIC, 'public').map((i) => i.label)
    assert.deepEqual(labels, ['Home', 'News', 'About', 'First'])
  } finally {
    console.warn = warn
  }
})

test('a module row carries its moduleId through, which is how the gate finds it', () => {
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas', feature: 'atlas' }] })
  const row = withModuleNav(PUBLIC, 'public').at(-1)
  assert.equal(row.moduleId, 'uo')
  assert.equal(row.feature, 'atlas')
})

test('a module row carries its icon through, and an override cannot touch it', () => {
  // 1.3.0. Without an `icon` the six extracted UO rows would have been the only
  // text-only entries in a sidebar where every other row has a glyph. Core
  // renders whatever component the row carries and supplies no fallback — an
  // invented one would be core making a presentation choice for content it knows
  // nothing about.
  const Glyph = () => null
  registerNav('uo', {
    area: 'admin',
    items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System', icon: Glyph }],
  })
  const merged = withModuleNav(ADMIN, 'admin')
  const row = merged.find((g) => g.title === 'System').items.find((i) => i.to === '/admin/uo/link')
  assert.equal(row.icon, Glyph)

  // `icon` was already on navOverrides' list of fields an override may not
  // touch, from long before a module could supply one. It still is.
  const overridden = applyNavOverrides(merged, { '/admin/uo/link': { label: 'Renamed', icon: 'nope' } })
  const after = overridden.find((g) => g.title === 'System').items.find((i) => i.to === '/admin/uo/link')
  assert.equal(after.label, 'Renamed')
  assert.equal(after.icon, Glyph)
})

test('a row with no icon simply has none, the same as a core row with none', () => {
  registerNav('uo', { area: 'player', items: [{ label: 'Characters', to: '/player/uo/characters', order: 0 }] })
  const row = withModuleNav([{ to: '/account', label: 'Account' }], 'player')[0]
  assert.equal(row.to, '/player/uo/characters')
  assert.equal(row.icon, undefined)
})

test('areas do not leak into one another', () => {
  registerNav('uo', { area: 'admin', items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System' }] })
  assert.equal(withModuleNav(PUBLIC, 'public'), PUBLIC)
})

// ── The relationship that is the actual requirement ───────────────────────

test('an admin override applies to a module row exactly as to a core row', () => {
  // The reason the interleave happens BEFORE the merge and not after: the merge
  // drops any key its base array does not declare, so appending module rows
  // afterwards would make every one of them unorderable, unrelabellable and
  // unhideable — a visible regression the day the UO rows leave core.
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas' }] })
  const base = withModuleNav(PUBLIC, 'public')
  const merged = applyNavOverrides(base, {
    '/uo/atlas': { label: 'Bestiary', order: 0 },
    '/site/news': { order: 3 },
  })
  assert.deepEqual(merged.map((i) => i.label), ['Bestiary', 'Home', 'About', 'News'])
})

test('an override can hide a module row, and the public tree can section it', () => {
  registerNav('uo', { area: 'public', items: [{ label: 'Atlas', to: '/uo/atlas' }, { label: 'Market', to: '/uo/market' }] })
  const base = withModuleNav(PUBLIC, 'public')

  const hidden = buildPublicNav(base, { '/uo/atlas': { hidden: true } })
  assert.equal(hidden.some((n) => n.to === '/uo/atlas'), false)

  const sectioned = buildPublicNav(base, {
    items: { '/uo/market': { section: 'sec_shard' } },
    sections: [{ id: 'sec_shard', label: 'Shard', order: 0 }],
  })
  assert.equal(sectioned[0].kind, 'section')
  assert.deepEqual(sectioned[0].items.map((i) => i.to), ['/uo/market'])
})

test('a module row can be moved between admin groups by an override', () => {
  registerNav('uo', { area: 'admin', items: [{ label: 'Shard', to: '/admin/uo/link', group: 'System' }] })
  const base = withModuleNav(ADMIN, 'admin')
  const merged = applyNavOverrides(base, { '/admin/uo/link': { group: 'Moderation' } })
  assert.deepEqual(merged[1].items.map((i) => i.to), ['/admin/moderation', '/admin/uo/link'])
  assert.deepEqual(merged[2].items.map((i) => i.to), ['/admin/users', '/admin/settings'])
})

test('a group a module created is itself a legal override destination', () => {
  // Falls out of building the destination set from the base nav it is handed —
  // recorded because it is the kind of thing that would otherwise be discovered
  // by an admin finding a section they cannot move anything into.
  registerNav('uo', { area: 'admin', items: [{ label: 'Atlas', to: '/admin/uo/atlas', group: 'Shard' }] })
  const base = withModuleNav(ADMIN, 'admin')
  const merged = applyNavOverrides(base, { '/admin/users': { group: 'Shard' } })
  assert.deepEqual(merged.at(-1).items.map((i) => i.to), ['/admin/uo/atlas', '/admin/users'])
})
