const { test, beforeEach } = require('node:test')
const assert = require('node:assert/strict')

const shardIngest = require('../src/utils/shardIngest')

// Protocol 3.0 points.board routing. Same shape as shardIngest.ruleset.test.js:
// stubbed deps, asserting where the dispatcher sends the frame and whether it is
// appended to the event log.
function makeDeps() {
  const calls = { boards: [], appended: [], broadcast: [] }
  const noop = async () => {}
  return {
    calls,
    shardEvents: { append: async (row) => { calls.appended.push(row); return true } },
    shardState: {
      upsertPointsBoard: async (ev) => { calls.boards.push(ev) },
      // Present so any stray routing is a harmless no-op.
      clearOnline: noop, upsertOnline: noop, setOffline: noop, upsertHouse: noop,
      addEconomySample: noop, setRuleset: noop,
    },
    shardLinks: { removeByAccount: noop },
    uoLinkConfig: { recordStatus: noop },
    broadcast: (ev) => { calls.broadcast.push(ev) },
    pushDispatch: async () => {},
    log: { warn() {}, info() {}, error() {} },
  }
}

const FRAME = {
  kind: 'points.board',
  t: 1000,
  system: 'QueensLoyalty',
  nameString: "Queen's Loyalty",
  nameNumber: 1114938,
  maxPoints: 30000,
  showOnGump: true,
  players: 842,
  top: [
    { rank: 1, serial: '0x1A2B', name: 'Darrow', points: 29500 },
    { rank: 2, serial: '0x1A2C', name: 'Mireille', points: 21000 },
  ],
}

beforeEach(() => shardIngest.reset())

test('points.board routes to upsertPointsBoard with the whole frame', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  assert.equal(deps.calls.boards.length, 1)
  const stored = deps.calls.boards[0]
  assert.equal(stored.system, 'QueensLoyalty')
  assert.equal(stored.nameNumber, 1114938)
  assert.equal(stored.players, 842)
  // The ranked list must survive intact — the read model serves it from the payload.
  assert.equal(stored.top.length, 2)
  assert.equal(stored.top[0].name, 'Darrow')
})

// A board is state, not an event. The shard emits a frame every time anyone's
// score moves the top ten, so logging would grow shard_events without bound for
// something whose only interesting value is its latest version — the same call
// guild.update already makes.
test('points.board is NOT appended to the event log', async () => {
  const deps = makeDeps()
  const r = await shardIngest.ingest(FRAME, deps)
  assert.equal(r.logged, false)
  assert.equal(deps.calls.appended.length, 0)
  assert.equal(shardIngest.LOGGED_KINDS.has('points.board'), false)
})

test('points.board is broadcast (the leaderboards page updates live)', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  assert.equal(deps.calls.broadcast.length, 1)
  assert.equal(deps.calls.broadcast[0].kind, 'points.board')
})

test('a backfilled points.board still stores but does not broadcast', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, { ...deps, fromBackfill: true })
  assert.equal(deps.calls.boards.length, 1)
  assert.equal(deps.calls.broadcast.length, 0)
})

// Each system is its own row, so two systems must not collide — this is the whole
// reason the frame is per-system rather than one board of everything.
test('two systems are stored independently', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  await shardIngest.ingest({ ...FRAME, system: 'CleanUpBritannia', nameString: null }, deps)
  assert.deepEqual(deps.calls.boards.map((b) => b.system), ['QueensLoyalty', 'CleanUpBritannia'])
})

// A re-emitted board is an overwrite of one row, never an append.
test('a repeated points.board overwrites rather than accumulating', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  await shardIngest.ingest({ ...FRAME, t: 2000, players: 843 }, deps)
  assert.equal(deps.calls.appended.length, 0)
  assert.equal(deps.calls.boards.length, 2) // two writes...
  assert.equal(deps.calls.boards[1].system, 'QueensLoyalty') // ...of the same row
})

// A model write that throws must not kill the feed.
test('an upsertPointsBoard failure does not throw or stop the broadcast', async () => {
  const deps = makeDeps()
  deps.shardState.upsertPointsBoard = async () => { throw new Error('db down') }
  const r = await shardIngest.ingest(FRAME, deps)
  assert.equal(r.logged, false)
  assert.equal(deps.calls.broadcast.length, 1)
})
