// Point the DB pool at a dead port before it's built; every pages.db method is
// monkeypatched below, and pool.close() at the end lets the process exit cleanly.
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

const { test, beforeEach, afterEach, after } = require('node:test')
const assert = require('node:assert/strict')
const pool = require('../src/utils/db')
after(() => pool.close())

// Unit-test the CMS pages model against an in-memory fake by monkeypatching
// pages.db (no DB). The point is to lock the rules the model owns and the API
// surface must not be able to bypass:
//   - slug validation + reserved-name guard, and slug immutability after create;
//   - block validation/sanitization on every write (the authoritative gate);
//   - the `protected` asymmetry: ON via a normal update, OFF only via unprotect();
//   - published_at stamped once, on the first publish;
//   - draft pages invisible to public (getBySlug) reads;
//   - a duplicate slug surfaces as a 409, not a raw DB error.
const pagesDb = require('../src/model/pages/pages.db')
const pages = require('../src/model/pages/pages.model')

let rows
let nextId
const saved = {}

// A row as pages.db would return it (snake_case columns). blocks stored as JSON.
function seed(row) {
  const full = {
    id: nextId++,
    slug: 'seed',
    title: 'Seed',
    status: 'draft',
    blocks: '[]',
    seo_title: null,
    meta_description: null,
    og_image: null,
    canonical_url: null,
    robots: null,
    layout: 'default',
    show_in_nav: 0,
    nav_group: null,
    nav_order: null,
    protected: 0,
    author_id: 1,
    created_at: new Date(),
    updated_at: new Date(),
    published_at: null,
    ...row,
  }
  rows.push(full)
  return full
}

beforeEach(() => {
  rows = []
  nextId = 1
  for (const k of ['listSummaries', 'findById', 'findBySlug', 'insert', 'update', 'remove']) saved[k] = pagesDb[k]

  pagesDb.listSummaries = async () => rows.slice()
  pagesDb.findById = async (id) => rows.find((r) => r.id === id) || null
  pagesDb.findBySlug = async (slug) => rows.find((r) => r.slug === slug) || null
  pagesDb.insert = async (row) => {
    if (rows.some((r) => r.slug === row.slug)) {
      const err = new Error('dup')
      err.code = 'ER_DUP_ENTRY'
      throw err
    }
    const id = nextId++
    rows.push({ id, created_at: new Date(), updated_at: new Date(), published_at: null, ...row })
    return id
  }
  pagesDb.update = async (id, fields) => {
    const row = rows.find((r) => r.id === id)
    if (row) Object.assign(row, fields)
  }
  pagesDb.remove = async (id) => {
    const i = rows.findIndex((r) => r.id === id)
    if (i >= 0) rows.splice(i, 1)
  }
})

afterEach(() => {
  for (const k of Object.keys(saved)) pagesDb[k] = saved[k]
})

// ── create: slug rules ─────────────────────────────────────────────────
test('create rejects a slug with illegal characters', async () => {
  await assert.rejects(
    () => pages.create({ slug: 'Not A Slug', title: 'T' }, 1),
    (e) => e.code === 'invalid_slug' && e.status === 400,
  )
})

test('create rejects a reserved slug that would shadow a named route', async () => {
  await assert.rejects(
    () => pages.create({ slug: 'admin', title: 'T' }, 1),
    (e) => e.code === 'reserved_slug' && e.status === 400,
  )
})

test('create requires a non-empty title within the length limit', async () => {
  await assert.rejects(() => pages.create({ slug: 'ok', title: '   ' }, 1), (e) => e.code === 'invalid_title')
  await assert.rejects(() => pages.create({ slug: 'ok', title: 'x'.repeat(201) }, 1), (e) => e.code === 'invalid_title')
})

test('create trims the title and defaults status to draft (no published_at)', async () => {
  const page = await pages.create({ slug: 'welcome', title: '  Welcome  ' }, 7)
  assert.equal(page.title, 'Welcome')
  assert.equal(page.status, 'draft')
  assert.equal(page.publishedAt, null)
  assert.equal(page.authorId, 7)
})

test('creating with status=published stamps published_at', async () => {
  const page = await pages.create({ slug: 'live', title: 'Live', status: 'published' }, 1)
  assert.equal(page.status, 'published')
  assert.ok(page.publishedAt instanceof Date)
})

test('a duplicate slug surfaces as a 409 slug_taken, not a raw DB error', async () => {
  await pages.create({ slug: 'dup', title: 'First' }, 1)
  await assert.rejects(
    () => pages.create({ slug: 'dup', title: 'Second' }, 1),
    (e) => e.code === 'slug_taken' && e.status === 409,
  )
})

// ── create: block gate ──────────────────────────────────────────────────
test('create rejects invalid blocks (the authoritative validation gate)', async () => {
  await assert.rejects(
    () => pages.create({ slug: 'bad', title: 'T', blocks: [{ type: 'does-not-exist' }] }, 1),
    (e) => e.code === 'invalid_blocks' && Array.isArray(e.errors) && e.errors.length > 0,
  )
})

// ── update: slug immutability ───────────────────────────────────────────
test('update rejects changing the slug after creation', async () => {
  const p = seed({ slug: 'fixed' })
  await assert.rejects(
    () => pages.update(p.id, { slug: 'renamed' }),
    (e) => e.code === 'slug_immutable' && e.status === 400,
  )
})

test('update tolerates the same slug being echoed back (no-op, not a rejection)', async () => {
  const p = seed({ slug: 'same' })
  const out = await pages.update(p.id, { slug: 'same', title: 'Updated' })
  assert.equal(out.title, 'Updated')
})

test('update on a missing page is a 404', async () => {
  await assert.rejects(() => pages.update(999, { title: 'x' }), (e) => e.code === 'not_found' && e.status === 404)
})

// ── update: publish stamping is once-only ───────────────────────────────
test('publishing stamps published_at once and does not re-stamp on a later edit', async () => {
  const p = seed({ slug: 'draft-first' })
  const published = await pages.update(p.id, { status: 'published' })
  const firstStamp = published.publishedAt
  assert.ok(firstStamp instanceof Date)

  // A later edit that keeps it published must not move published_at.
  await pages.update(p.id, { title: 'Edited' })
  const again = await pages.getById(p.id)
  assert.deepEqual(again.publishedAt, firstStamp)
})

// ── the protected asymmetry (a security boundary) ───────────────────────
test('update can turn protection ON', async () => {
  const p = seed({ slug: 'guard', protected: 0 })
  const out = await pages.update(p.id, { settings: { protected: true } })
  assert.equal(out.settings.protected, true)
})

test('update CANNOT turn protection OFF — that requires the unprotect endpoint', async () => {
  const p = seed({ slug: 'guarded', protected: 1 })
  await assert.rejects(
    () => pages.update(p.id, { settings: { protected: false } }),
    (e) => e.code === 'unprotect_required' && e.status === 403,
  )
})

test('setting protected=false on an already-unprotected page is a harmless no-op', async () => {
  const p = seed({ slug: 'open', protected: 0 })
  const out = await pages.update(p.id, { settings: { protected: false } })
  assert.equal(out.settings.protected, false)
})

test('unprotect() is the only path that clears protection', async () => {
  const p = seed({ slug: 'locked', protected: 1 })
  const out = await pages.unprotect(p.id)
  assert.equal(out.settings.protected, false)
})

// ── delete guard ────────────────────────────────────────────────────────
test('a protected page cannot be deleted', async () => {
  const p = seed({ slug: 'keep', protected: 1 })
  await assert.rejects(() => pages.remove(p.id), (e) => e.code === 'page_protected' && e.status === 403)
  assert.ok(rows.find((r) => r.id === p.id), 'row still present')
})

test('an unprotected page deletes', async () => {
  const p = seed({ slug: 'trash', protected: 0 })
  const out = await pages.remove(p.id)
  assert.equal(out.id, p.id)
  assert.equal(rows.find((r) => r.id === p.id), undefined)
})

// ── public read hides drafts ────────────────────────────────────────────
test('getBySlug hides a draft from the public but an admin can include it', async () => {
  seed({ slug: 'hidden', status: 'draft' })
  assert.equal(await pages.getBySlug('hidden'), null) // public: indistinguishable from missing
  const asAdmin = await pages.getBySlug('hidden', { includeUnpublished: true })
  assert.equal(asAdmin.slug, 'hidden')
})

test('getBySlug returns a published page to the public', async () => {
  seed({ slug: 'shown', status: 'published' })
  const out = await pages.getBySlug('shown')
  assert.equal(out.slug, 'shown')
})

// ── field-mapping validation ────────────────────────────────────────────
test('update rejects an unknown layout and an out-of-range metadata string', async () => {
  const p = seed({ slug: 'meta' })
  await assert.rejects(() => pages.update(p.id, { settings: { layout: 'fancy' } }), (e) => e.code === 'invalid_settings')
  await assert.rejects(
    () => pages.update(p.id, { metadata: { seoTitle: 'x'.repeat(201) } }),
    (e) => e.code === 'invalid_metadata',
  )
})

test('serialize maps DB columns to the grouped API shape and coerces flags to booleans', async () => {
  const p = seed({ slug: 'shape', show_in_nav: 1, protected: 1, nav_group: 'main', nav_order: 3 })
  const out = await pages.getById(p.id)
  assert.equal(out.settings.showInNav, true)
  assert.equal(out.settings.protected, true)
  assert.equal(out.settings.navGroup, 'main')
  assert.equal(out.settings.navOrder, 3)
  assert.equal(out.metadata.seoTitle, null)
})
