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

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

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

const FRAME = {
  kind: 'world.ruleset',
  t: 1000,
  rev: '1a2b3c4d',
  shard: 'UOMysticmoon',
  expansion: 'EJ',
  systems: { cityLoyalty: true, vvv: true, factions: false },
  caps: { skill: 1000, totalSkill: 7000 },
}

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

test('world.ruleset routes to setRuleset with the whole frame', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  assert.equal(deps.calls.rulesetSet.length, 1)
  const stored = deps.calls.rulesetSet[0]
  assert.equal(stored.rev, '1a2b3c4d')
  assert.equal(stored.expansion, 'EJ')
  // The nested blocks must survive intact — the read model serves the frame whole.
  assert.equal(stored.systems.vvv, true)
  assert.equal(stored.caps.totalSkill, 7000)
})

// The shard re-emits world.ruleset on EVERY sidecar connect. Logging it would put
// a duplicate row in shard_events per reconnect, and server.hello already marks
// each of those — so this assertion is the guard on that decision.
test('world.ruleset 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('world.ruleset'), false)
})

test('world.ruleset is broadcast (the rules 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, 'world.ruleset')
})

// A backfill replay must reach the store but must NOT re-animate the live ticker.
test('a backfilled world.ruleset still stores but does not broadcast', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, { ...deps, fromBackfill: true })
  assert.equal(deps.calls.rulesetSet.length, 1)
  assert.equal(deps.calls.broadcast.length, 0)
})

// A re-emitted identical ruleset is an overwrite, not an append: two ingests of
// the same rev leave one stored frame's worth of state, never a growing log.
test('a repeated world.ruleset overwrites rather than accumulating', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, deps)
  await shardIngest.ingest({ ...FRAME, t: 2000 }, deps)
  assert.equal(deps.calls.appended.length, 0)
  assert.equal(deps.calls.rulesetSet.length, 2) // two writes...
  assert.equal(deps.calls.rulesetSet[1].rev, '1a2b3c4d') // ...of the same singleton
})

// A model write that throws must not kill the feed — ingest swallows it and the
// frame is still broadcast.
test('a setRuleset failure does not throw or stop the broadcast', async () => {
  const deps = makeDeps()
  deps.shardState.setRuleset = 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)
})

// ── Shard name fallback ────────────────────────────────────────────────────
//
// ServUO ships Server.cfg with `Name=My Shard`. An operator who never edited it
// publishes that verbatim, which says "unnamed" rather than naming anything — so
// the site answers with its own instance name instead of printing the stock
// default under a header carrying the real one.
//
// Applied at INGEST, not on read, because world.ruleset is also broadcast live:
// the same object goes to the SSE fan-out, so a read-time fix would be undone by
// the next reconnect's frame. These tests assert both halves.

function withSettings(deps, name) {
  return { ...deps, settings: { getInstanceName: async () => name } }
}

test('the stock ServUO shard name is replaced with the instance name', async () => {
  const deps = makeDeps()
  await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, withSettings(deps, 'UOMysticmoon'))
  assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
})

test('the substituted name reaches the live broadcast, not just the store', async () => {
  const deps = makeDeps()
  await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, withSettings(deps, 'UOMysticmoon'))
  assert.equal(deps.calls.broadcast.length, 1)
  assert.equal(deps.calls.broadcast[0].shard, 'UOMysticmoon')
})

test('a missing or blank shard name gets the same treatment', async () => {
  for (const shard of [undefined, null, '', '   ']) {
    const deps = makeDeps()
    await shardIngest.ingest({ ...FRAME, shard }, withSettings(deps, 'UOMysticmoon'))
    assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
  }
})

// The match is on the whole value, case- and padding-insensitive. A shard that
// deliberately calls itself "My Shard Reborn" has named itself and keeps it.
test('a real name that merely contains the stock one is left alone', async () => {
  const deps = makeDeps()
  await shardIngest.ingest({ ...FRAME, shard: 'My Shard Reborn' }, withSettings(deps, 'UOMysticmoon'))
  assert.equal(deps.calls.rulesetSet[0].shard, 'My Shard Reborn')

  const padded = makeDeps()
  await shardIngest.ingest({ ...FRAME, shard: '  MY SHARD  ' }, withSettings(padded, 'UOMysticmoon'))
  assert.equal(padded.calls.rulesetSet[0].shard, 'UOMysticmoon')
})

test('a shard that named itself is never overridden by the brand', async () => {
  const deps = makeDeps()
  await shardIngest.ingest(FRAME, withSettings(deps, 'Some Other Brand'))
  assert.equal(deps.calls.rulesetSet[0].shard, 'UOMysticmoon')
})

// The settings read is a DB call on a path that must never fail ingest.
test('a settings read failure leaves the frame storable', async () => {
  const deps = makeDeps()
  const boom = { ...deps, settings: { getInstanceName: async () => { throw new Error('db down') } } }
  await shardIngest.ingest({ ...FRAME, shard: 'My Shard' }, boom)
  assert.equal(deps.calls.rulesetSet.length, 1)
  assert.equal(deps.calls.rulesetSet[0].shard, 'My Shard')
})
