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

import {
  registry,
  declareSlot,
  registerExtension,
  extensionFor,
  registeredIds,
  _reset,
} from '../src/modules/registry.js'

// Client extension slots (docs/website/MODULE_API.md §3.7) — the client twin of
// the server's declareSlot/registerExtension.
//
// The registry half only. `<Slot>` itself renders, and there is no DOM in this
// runner, so what it does with what these functions return — including the error
// boundary — is proved by the §7.7 browser smoke instead. Everything below is a
// rule that can be stated without rendering anything, and every one of them can
// be got wrong in a way a browser check would not obviously catch.

beforeEach(() => _reset())

const Fake = () => null
const Other = () => null

test('an unfilled slot reads as nothing', () => {
  // The guarantee core's layouts rest on: place a slot, install no module, and
  // the page renders what it rendered before.
  declareSlot('site.footer.status')
  assert.equal(extensionFor('site.footer.status'), null)
})

test('an undeclared slot reads as nothing rather than throwing', () => {
  // Reading is core's side and stays fail-safe: a typo in a layout costs that
  // spot, not the page. Only WRITING is strict, which is the next test.
  assert.equal(extensionFor('nope'), null)
})

test('a module fills a declared slot and core reads it back', () => {
  declareSlot('admin.users.detail')
  registerExtension('uo', 'admin.users.detail', Fake)
  assert.equal(extensionFor('admin.users.detail'), Fake)
  assert.deepEqual(registeredIds(), ['uo'])
})

test('filling an unknown slot throws, naming the slot', () => {
  // This is the one place the client registry is NOT fail-open, and the reason
  // is asymmetry of consequence: a dropped nav row costs a link the viewer can
  // reach another way, a silently dropped extension is invisible to everyone
  // including its author. Declaration structurally precedes filling (§3.1), so
  // this can only ever be a typo or a version skew.
  assert.throws(() => registerExtension('uo', 'site.footer.sttaus', Fake), /unknown extension slot "site\.footer\.sttaus"/)
})

test('a non-component fill throws', () => {
  declareSlot('site.footer.status')
  assert.throws(() => registerExtension('uo', 'site.footer.status', { render: true }), /is not a component/)
})

test('a second module cannot take a filled slot, and the first keeps it', () => {
  // Matches the server's rule exactly (registries.js): first fill wins, second
  // is an error. The second half of the assertion is the one that matters — a
  // rejected fill must not have half-replaced the incumbent.
  declareSlot('admin.users.detail')
  registerExtension('uo', 'admin.users.detail', Fake)
  assert.throws(() => registerExtension('other', 'admin.users.detail', Other), /already filled by "uo"/)
  assert.equal(extensionFor('admin.users.detail'), Fake)
})

test('declaring a slot twice throws', () => {
  // Core-side programming error: two owners for one position means whichever
  // module registered first wins by file order.
  declareSlot('site.footer.status')
  assert.throws(() => declareSlot('site.footer.status'), /already declared/)
})

test('core fills a slot through the same seam a module uses', () => {
  // The client twin of registries.registerCore(). Core is a registrant with an
  // id like any other, which is what makes slice 3 a deletion: the module
  // registers the same slot and core drops its line.
  declareSlot('site.footer.status')
  registerExtension('core', 'site.footer.status', Fake)
  assert.deepEqual(registeredIds(), ['core'])
})

test('declareSlot and extensionFor are not on the module-facing registry', () => {
  // Declaring is core's alone (§3.7), and reading who filled a slot is core's
  // too — the same line featureProviders() draws. registerExtension IS on the
  // object, because filling is the whole point.
  assert.equal(registry.declareSlot, undefined)
  assert.equal(registry.extensionFor, undefined)
  assert.equal(typeof registry.registerExtension, 'function')
})
