// Point the DB at a closed port BEFORE requiring the controller (its 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')

// Unit-test the public CMS/wiki controller's decision logic:
//   - getPage: staff see drafts (live preview); the public gets a 404 for a draft,
//     indistinguishable from a missing page (draft visibility is a boundary);
//   - getPagePreview: only a valid, matching preview token unlocks a draft;
//   - getWikiList: full-text search takes precedence, and an unknown category/tag
//     yields [] rather than an error;
//   - getPost(s): an unknown category is a 404;
//   - contact: a mailer failure surfaces as a 502, not a 500 or a throw.
const ctrl = require('../src/router/v1/public/public.controller')
const posts = require('../src/model/posts/posts.model')
const wiki = require('../src/model/wiki/wiki.model')
const pages = require('../src/model/pages/pages.model')
const mailer = require('../src/utils/mailer')
const token = require('../src/auth/token')
const sessionService = require('../src/auth/session.service')
const db = require('../src/utils/db')

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

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

// getUserFromRequest (destructured into the controller) delegates to
// sessionService.validateSession — drive staff/public from there.
function asStaff(role = 'admin') {
  sessionService.validateSession = () => ({ userId: 1, username: 'boss', role })
}
function asPublic() {
  sessionService.validateSession = () => null
}

const originals = {
  validateSession: sessionService.validateSession,
  getBySlug: pages.getBySlug,
  getById: pages.getById,
  isValidUrlCategory: posts.isValidUrlCategory,
  listPublished: posts.listPublished,
  getPublished: posts.getPublished,
  wikiSearch: wiki.search,
  getCategoryBySlug: wiki.getCategoryBySlug,
  getTagBySlug: wiki.getTagBySlug,
  wikiListPublished: wiki.listPublished,
  sendContactMessage: mailer.sendContactMessage,
}
afterEach(() => {
  sessionService.validateSession = originals.validateSession
  pages.getBySlug = originals.getBySlug
  pages.getById = originals.getById
  posts.isValidUrlCategory = originals.isValidUrlCategory
  posts.listPublished = originals.listPublished
  posts.getPublished = originals.getPublished
  wiki.search = originals.wikiSearch
  wiki.getCategoryBySlug = originals.getCategoryBySlug
  wiki.getTagBySlug = originals.getTagBySlug
  wiki.listPublished = originals.wikiListPublished
  mailer.sendContactMessage = originals.sendContactMessage
})

// ── getPage draft visibility ────────────────────────────────────────────
test('getPage lets staff include unpublished drafts', async () => {
  asStaff('editor')
  let sawOpts
  pages.getBySlug = async (slug, opts) => {
    sawOpts = opts
    return { slug, status: 'draft' }
  }
  const res = mockRes()
  await ctrl.getPage({ params: { slug: 'wip' } }, res)
  assert.equal(sawOpts.includeUnpublished, true)
  assert.equal(res.body.slug, 'wip')
})

test('getPage hides drafts from the public and 404s (model returns null)', async () => {
  asPublic()
  let sawOpts
  pages.getBySlug = async (slug, opts) => {
    sawOpts = opts
    return null // model already filtered the draft out for a public caller
  }
  const res = mockRes()
  await ctrl.getPage({ params: { slug: 'wip' } }, res)
  assert.equal(sawOpts.includeUnpublished, false)
  assert.equal(res.statusCode, 404)
})

test('getPage treats a player role as non-staff (no draft access)', async () => {
  asStaff('player') // a player is NOT in STAFF_ROLES
  let sawOpts
  pages.getBySlug = async (slug, opts) => {
    sawOpts = opts
    return null
  }
  await ctrl.getPage({ params: { slug: 'wip' } }, mockRes())
  assert.equal(sawOpts.includeUnpublished, false)
})

// ── getPagePreview token gate ───────────────────────────────────────────
test('getPagePreview unlocks a draft with a valid, matching preview token', async () => {
  asPublic()
  const validToken = token.signPagePreview(42)
  pages.getById = async (id) => ({ id, status: 'draft' })
  const res = mockRes()
  await ctrl.getPagePreview({ params: { id: '42', token: validToken } }, res)
  assert.equal(res.statusCode, 200)
  assert.equal(res.body.id, 42)
})

test('getPagePreview 404s when the token is for a different page', async () => {
  const tokenForOther = token.signPagePreview(7)
  let loaded = false
  pages.getById = async () => {
    loaded = true
    return { id: 42 }
  }
  const res = mockRes()
  await ctrl.getPagePreview({ params: { id: '42', token: tokenForOther } }, res)
  assert.equal(res.statusCode, 404)
  assert.equal(loaded, false, 'a mismatched token never loads the page')
})

test('getPagePreview 404s on a garbage token', async () => {
  const res = mockRes()
  await ctrl.getPagePreview({ params: { id: '42', token: 'not-a-jwt' } }, res)
  assert.equal(res.statusCode, 404)
})

// ── getWikiList precedence + unknown filters ────────────────────────────
test('getWikiList runs a full-text search when q is present, ignoring filters', async () => {
  let searched
  wiki.search = async (q, opts) => {
    searched = { q, opts }
    return [{ slug: 'hit' }]
  }
  wiki.listPublished = async () => {
    throw new Error('listPublished should not run when q is set')
  }
  const res = mockRes()
  await ctrl.getWikiList({ query: { q: '  dragon  ', category: 'bestiary' } }, res)
  assert.equal(searched.q, 'dragon') // trimmed
  assert.equal(searched.opts.publishedOnly, true)
  assert.equal(res.body[0].slug, 'hit')
})

test('getWikiList returns [] for an unknown category filter without listing pages', async () => {
  wiki.getCategoryBySlug = async () => null
  let listed = false
  wiki.listPublished = async () => {
    listed = true
    return []
  }
  const res = mockRes()
  await ctrl.getWikiList({ query: { category: 'ghosts' } }, res)
  assert.deepEqual(res.body, [])
  assert.equal(listed, false)
})

test('getWikiList combines a known category and tag into the list filter', async () => {
  wiki.getCategoryBySlug = async () => ({ id: 3 })
  wiki.getTagBySlug = async () => ({ id: 9 })
  let filters
  wiki.listPublished = async (f) => {
    filters = f
    return []
  }
  await ctrl.getWikiList({ query: { category: 'lore', tag: 'undead' } }, mockRes())
  assert.deepEqual(filters, { categoryId: 3, tagId: 9 })
})

// ── posts category validation ───────────────────────────────────────────
test('getPosts 404s an unknown url category', async () => {
  posts.isValidUrlCategory = () => false
  const res = mockRes()
  await ctrl.getPosts({ params: { category: 'nope' } }, res)
  assert.equal(res.statusCode, 404)
})

test('getPost 404s a valid category with no matching post', async () => {
  posts.isValidUrlCategory = () => true
  posts.getPublished = async () => null
  const res = mockRes()
  await ctrl.getPost({ params: { category: 'news', idOrSlug: 'missing' } }, res)
  assert.equal(res.statusCode, 404)
})

// ── contact failure path ────────────────────────────────────────────────
test('contact surfaces a mailer failure as a 502 (not a 500 or a throw)', async () => {
  mailer.sendContactMessage = async () => {
    throw new Error('smtp down')
  }
  const res = mockRes()
  await ctrl.contact({ body: { name: 'A', email: 'a@b.c', message: 'hi' } }, res)
  assert.equal(res.statusCode, 502)
  assert.match(res.body.message, /send/i)
})
