// Point the DB at a closed port BEFORE requiring the controllers (their models
// build the pool). Every model call is monkeypatched, so no query runs;
// db.close() at the end releases the pool so the process exits cleanly.
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

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

// The spawn atlas API, public and admin. What is worth asserting here is not the
// SQL (that is the parser suite's job) but the contracts the two surfaces make:
//
//   • the public reads project through the visibility framework — v3.md §3.6.1's
//     standing rule is that a read path returning shard data and not calling
//     projectFeature is a bug, and `atlas` declaring no sensitive fields TODAY is
//     exactly why the call has to be there before one does;
//   • the public /meta route reports the game world only, never the operator's
//     filesystem — the ServUO path, the per-file hashes and any pending refresh
//     stay on the admin route;
//   • a missing creature is a 404, not an empty 200;
//   • an unreadable ServUO tree is a 200 carrying `status: 'unavailable'`, NOT a
//     500. The refresh contract reports outcomes rather than throwing (so boot is
//     never blocked by a bad tree), and the admin needs to be told what is wrong
//     with their path;
//   • a model failure degrades to a 500 rather than a thrown/uncaught error.
const pub = require('../src/router/v1/public/atlas.controller')
const admin = require('../src/router/v1/admin/shardAtlas.controller')
const atlas = require('../src/model/shardAtlas/shardAtlas.model')
const activity = require('../src/model/activity/activity.model')
const visibility = require('../src/utils/shardVisibility')
const db = require('../src/utils/db')

after(() => db.close())

// Stub the visibility MODEL rather than the util's exports: project() calls the
// module-internal getConfig, which an exports-level stub would not intercept — it
// would hit the closed DB port and cost a ~10s pool timeout per test before
// falling back to these same defaults.
const visibilityModel = require('../src/model/shardVisibility/shardVisibility.model')
visibilityModel.listAll = async () => [] // no overrides ⇒ compiled defaults
visibility.viewerLevel = async (req) => req?.viewerLevel || 'anonymous'

// The admin controller logs every action; keep it off the DB.
activity.log = async () => {}

function mockRes() {
  return {
    statusCode: 200,
    body: null,
    status(c) {
      this.statusCode = c
      return this
    },
    json(b) {
      this.body = b
      return this
    },
  }
}

const originals = {
  searchCreatures: atlas.searchCreatures,
  getCreature: atlas.getCreature,
  listRegions: atlas.listRegions,
  listLandmarks: atlas.listLandmarks,
  listChampions: atlas.listChampions,
  publicMeta: atlas.publicMeta,
  status: atlas.status,
  refresh: atlas.refresh,
  approvePending: atlas.approvePending,
  rejectPending: atlas.rejectPending,
  setServuoPath: atlas.setServuoPath,
}
afterEach(() => Object.assign(atlas, originals))

// ── Public reads ────────────────────────────────────────────────────────
test('getCreatures passes the search through and returns the page shape', async () => {
  let seen = null
  atlas.searchCreatures = async (opts) => {
    seen = opts
    return { total: 1, limit: 50, offset: 0, creatures: [{ slug: 'lizardman', name: 'Lizardman' }] }
  }
  const res = mockRes()
  await pub.getCreatures({ query: { q: ' lizard ', facet: 'Felucca', limit: '10', offset: '20' } }, res)
  assert.deepEqual(seen, { q: 'lizard', facet: 'Felucca', limit: 10, offset: 20 })
  assert.equal(res.body.total, 1)
  assert.equal(res.body.creatures[0].slug, 'lizardman')
})

test('getCreatures falls back to the documented defaults when nothing is passed', async () => {
  let seen = null
  atlas.searchCreatures = async (opts) => {
    seen = opts
    return { total: 0, limit: 50, offset: 0, creatures: [] }
  }
  await pub.getCreatures({ query: {} }, mockRes())
  assert.deepEqual(seen, { q: '', facet: '', limit: 50, offset: 0 })
})

test('an unknown creature is a 404, not an empty 200', async () => {
  atlas.getCreature = async () => null
  const res = mockRes()
  await pub.getCreature({ params: { slug: 'nosuchthing' }, query: {} }, res)
  assert.equal(res.statusCode, 404)
})

test('getCreature returns places and spawners, and `points` stays the COUNT', async () => {
  atlas.getCreature = async () => ({
    slug: 'lizardman',
    name: 'Lizardman',
    total: 214,
    points: 62,
    places: [{ facet: 'Trammel', label: 'Shrines', spawners: 7, maxAlive: 21 }],
    spawners: [{ id: 1, facet: 'Trammel', label: 'Shrines', x: 1, y: 2 }],
    spawnersTruncated: false,
    alsoHere: [],
  })
  const res = mockRes()
  await pub.getCreature({ params: { slug: 'lizardman' }, query: {} }, res)
  // The list route uses `points` as a number; the detail route must not quietly
  // turn the same key into an array.
  assert.equal(typeof res.body.points, 'number')
  assert.ok(Array.isArray(res.body.spawners))
  assert.equal(res.body.places[0].label, 'Shrines')
})

// ── The projection rule (§3.6.1) ────────────────────────────────────────
test('public reads run through projectFeature, so a locked field can never survive', async () => {
  // `atlas` declares no sensitive fields, so nothing here is stripped by a
  // FEATURE rule. acct/webId are stripped anyway — they are locked by meaning,
  // for every feature, and this is what proves the read path projects at all.
  atlas.searchCreatures = async () => ({
    total: 1,
    limit: 50,
    offset: 0,
    creatures: [{ slug: 'lizardman', name: 'Lizardman', acct: 'someacct', ownerWebId: 7 }],
  })
  const res = mockRes()
  await pub.getCreatures({ query: {}, viewerLevel: 'anonymous' }, res)
  const row = res.body.creatures[0]
  assert.equal(row.name, 'Lizardman')
  assert.ok(!('acct' in row), 'acct must never reach an anonymous caller')
  assert.ok(!('ownerWebId' in row), 'a flattened webId spelling is locked too')
})

test('getMeta reports the game world only — never the operator’s filesystem', async () => {
  // The model is what enforces this; the assertion documents the boundary so a
  // future "just return status() here" shortcut fails loudly.
  atlas.publicMeta = async () => ({
    importedAt: '2026-07-28T00:00:00.000Z',
    generatedAt: '2026-07-28T00:00:00.000Z',
    counts: { points: 6455, creatures: 800 },
    facets: ['Felucca', 'Trammel'],
  })
  const res = mockRes()
  await pub.getMeta({ query: {} }, res)
  assert.deepEqual(Object.keys(res.body).sort(), ['counts', 'facets', 'generatedAt', 'importedAt'])
  assert.ok(!('path' in res.body))
  assert.ok(!('pending' in res.body))
})

test('a model failure degrades to a 500 rather than throwing', async () => {
  atlas.listChampions = async () => {
    throw new Error('table is gone')
  }
  const res = mockRes()
  await pub.getChampions({ query: {} }, res)
  assert.equal(res.statusCode, 500)
})

// ── Admin ───────────────────────────────────────────────────────────────
test('an unreadable tree answers 200 with the reason, not a 500', async () => {
  atlas.refresh = async () => ({ status: 'unavailable', reason: 'no Spawns directory', path: '/bad' })
  const res = mockRes()
  await admin.importAtlas({ body: {}, user: { id: 1 } }, res)
  assert.equal(res.statusCode, 200)
  assert.equal(res.body.status, 'unavailable')
  assert.equal(res.body.reason, 'no Spawns directory')
})

test('import passes `force` through and coerces it to a boolean', async () => {
  let seen = null
  atlas.refresh = async (opts) => {
    seen = opts
    return { status: 'unchanged' }
  }
  await admin.importAtlas({ body: { force: true }, user: { id: 1 } }, mockRes())
  assert.deepEqual(seen, { force: true })
})

test('approve applies a staged refresh (facet loss included)', async () => {
  let called = false
  atlas.approvePending = async () => {
    called = true
    return { status: 'imported', removedFacets: ['Malas'], counts: { points: 6162 } }
  }
  const res = mockRes()
  await admin.approve({ user: { id: 1 } }, res)
  assert.ok(called)
  assert.equal(res.body.status, 'imported')
})

test('rejecting when nothing is staged is a 404', async () => {
  atlas.rejectPending = async () => ({ status: 'none' })
  const res = mockRes()
  await admin.reject({ user: { id: 1 } }, res)
  assert.equal(res.statusCode, 404)
})

test('setPath trims, persists, and answers with fresh status — it does not import', async () => {
  let saved = null
  let imported = false
  atlas.setServuoPath = async (value) => {
    saved = value
  }
  atlas.refresh = async () => {
    imported = true
    return { status: 'imported' }
  }
  atlas.status = async () => ({ configured: true, path: '/srv/servuo', treeReadable: true })
  const res = mockRes()
  await admin.setPath({ body: { path: '  /srv/servuo  ' }, user: { id: 3 } }, res)
  assert.equal(saved, '/srv/servuo')
  assert.equal(imported, false, 'changing the path must not reload the atlas as a side effect')
  assert.equal(res.body.path, '/srv/servuo')
})

test('setPath accepts a blank path (clearing it turns the atlas off)', async () => {
  let saved = 'unset'
  atlas.setServuoPath = async (value) => {
    saved = value
  }
  atlas.status = async () => ({ configured: false, path: '' })
  const res = mockRes()
  await admin.setPath({ body: {}, user: { id: 3 } }, res)
  assert.equal(saved, '')
  assert.equal(res.statusCode, 200)
})
