/**
 * Gate-3 consumer harness for @platform-modules/comments.
 *
 * A REAL consumer reconstructs the comments table from commentsTableSql (the public DDL helper)
 * and drives the full write contract through the PUBLIC barrel ONLY — proving exports/types/
 * runtime integration, the hard-floor contracts, and clean removal.
 *
 * Mirrors: apps/consumer/tests/content.test.ts
 */
import { PGlite } from '@electric-sql/pglite'
import { drizzle } from 'drizzle-orm/pglite'
import { sql } from 'drizzle-orm'
import { beforeEach, describe, expect, it } from 'vitest'
import type { Querier } from '@platform-modules/db'
import {
  comments,
  commentsSchema,
  commentsTableSql,
  count,
  edit,
  getById,
  isCommentError,
  list,
  post,
  remove,
  setStatus,
  CommentAuthzError,
  CommentSanitizationError,
  CommentValidationError,
  type Actor,
  type CommentsSchema,
  type CommentTarget,
} from '@platform-modules/comments'

type CommentsDb = Querier<CommentsSchema>

// Deterministic fake sanitizer (no DOMPurify dep — spec §3)
function sanitize(raw: string): string {
  return raw
    .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, '')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
}

const target: CommentTarget = { type: 'post', id: 'consumer-p1' }
const author: Actor = { id: 'consumer-u1' }
const moderator: Actor = { id: 'consumer-mod', canModerate: true }

async function createCommentsDb(): Promise<{ db: CommentsDb; client: PGlite }> {
  const client = new PGlite()
  const db = drizzle(client, { schema: commentsSchema }) as unknown as CommentsDb
  // Reconstruct from the public DDL helper — this is the real host integration path
  await client.exec(commentsTableSql())
  return { db, client }
}

let db: CommentsDb
let client: PGlite

beforeEach(async () => {
  ;({ db, client } = await createCommentsDb())
})

describe('comments consumer fixture (Gate 3 — full write contract through the public barrel)', () => {
  // ── basic lifecycle ─────────────────────────────────────────────────────
  it('post → list round-trip with stored depth', async () => {
    const ref = await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'hello gate3' }, author, sanitize)
    expect(ref.id).toBeTruthy()
    expect(ref.target).toEqual(target)

    // pending by default — list as moderator
    const page = await list(db, target, { status: 'pending' }, moderator)
    expect(page.items).toHaveLength(1)
    expect(page.items[0]!.depth).toBe(0)
    expect(page.items[0]!.body).toBe('hello gate3')
  })

  // ── thread nesting + depth-cap-at-5 ────────────────────────────────────
  it('thread nesting + depth-cap-at-5', async () => {
    let parentId: string | null = null
    for (let i = 0; i < 7; i++) {
      const ref = await post(
        db,
        { target, author: { kind: 'user', userId: 'u1' }, body: `lvl ${i}`, parentId: parentId ?? undefined },
        author,
        sanitize,
      )
      parentId = ref.id
    }
    const page = await list(db, target, { status: 'pending', limit: 20 }, moderator)
    const depths = page.items.map((c) => c.depth)
    expect(depths.slice(0, 5)).toEqual([0, 1, 2, 3, 4])
    expect(depths[5]).toBe(5)
    expect(depths[6]).toBe(5)
  })

  // ── moderation default = pending ────────────────────────────────────────
  it('moderation default = pending', async () => {
    const ref = await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'test' }, author, sanitize)
    const c = await getById(db, ref.id)
    expect(c!.status).toBe('pending')
  })

  // ── non-moderator initialStatus:published → CommentAuthzError ──────────
  it("non-moderator initialStatus:'published' → CommentAuthzError", async () => {
    await expect(
      post(
        db,
        { target, author: { kind: 'user', userId: 'u1' }, body: 'hi', initialStatus: 'published' },
        author,
        sanitize,
      ),
    ).rejects.toThrow(CommentAuthzError)
  })

  // ── setStatus gated on canModerate ─────────────────────────────────────
  it('setStatus approve/spam/trash gated on canModerate', async () => {
    const ref = await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'hi' }, author, sanitize)
    await expect(setStatus(db, ref.id, 'published', author)).rejects.toThrow(CommentAuthzError)
    await setStatus(db, ref.id, 'published', moderator)
    const c = await getById(db, ref.id)
    expect(c!.status).toBe('published')
  })

  // ── IDOR: non-author non-moderator cannot edit/remove ──────────────────
  it('IDOR — non-author non-moderator cannot edit', async () => {
    const ref = await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'mine' }, author, sanitize)
    const stranger: Actor = { id: 'stranger' }
    await expect(edit(db, ref.id, 'hacked', stranger, sanitize)).rejects.toThrow(CommentAuthzError)
  })

  it('IDOR — non-author non-moderator cannot remove', async () => {
    const ref = await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'mine' }, author, sanitize)
    const stranger: Actor = { id: 'stranger' }
    await expect(remove(db, ref.id, stranger)).rejects.toThrow(CommentAuthzError)
  })

  it('IDOR — guest-authored comment: only moderator can modify', async () => {
    const ref = await post(
      db,
      { target, author: { kind: 'guest', name: 'Alice' }, body: 'guest post' },
      null,
      sanitize,
    )
    await expect(edit(db, ref.id, 'hack', author, sanitize)).rejects.toThrow(CommentAuthzError)
    await expect(remove(db, ref.id, author)).rejects.toThrow(CommentAuthzError)
  })

  // ── stored-XSS: <script> body → sanitized bodyHtml ─────────────────────
  it('stored-XSS: <script> body returns sanitized bodyHtml', async () => {
    const ref = await post(
      db,
      { target, author: { kind: 'user', userId: 'u1' }, body: '<script>alert(1)</script>hi' },
      author,
      sanitize,
    )
    const c = await getById(db, ref.id)
    expect(c!.bodyHtml).not.toContain('<script>')
    expect(c!.body).toContain('<script>') // raw body preserved
  })

  it('missing/non-function sanitize → CommentSanitizationError', async () => {
    await expect(
      post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'hi' }, author, undefined),
    ).rejects.toThrow(CommentSanitizationError)
    await expect(
      post(db, { target, author: { kind: 'user', userId: 'u1' }, body: 'hi' }, author, 42 as never),
    ).rejects.toThrow(CommentSanitizationError)
  })

  // ── edit re-sanitizes + bumps editedAt ─────────────────────────────────
  it('edit re-sanitizes bodyHtml and bumps editedAt', async () => {
    // author.id = 'consumer-u1'; post with matching userId so authz passes
    const ref = await post(db, { target, author: { kind: 'user', userId: 'consumer-u1' }, body: 'original' }, author, sanitize)
    const before = await getById(db, ref.id)
    expect(before!.editedAt).toBeNull()
    await edit(db, ref.id, 'updated<script>evil</script>', author, sanitize)
    const after = await getById(db, ref.id)
    expect(after!.bodyHtml).not.toContain('<script>')
    expect(after!.editedAt).not.toBeNull()
  })

  // ── guest-email redaction ──────────────────────────────────────────────
  it('guest email redacted from public read (url retained)', async () => {
    const ref = await post(
      db,
      { target, author: { kind: 'guest', name: 'Bob', email: 'bob@secret.com', url: 'https://bob.dev' }, body: 'hi' },
      null,
      sanitize,
    )
    const pub = await getById(db, ref.id)
    expect(JSON.stringify(pub!.author)).not.toContain('bob@secret.com')
    expect(JSON.stringify(pub!.author)).toContain('https://bob.dev')

    const mod = await getById(db, ref.id, moderator)
    expect((mod!.author as { email?: string }).email).toBe('bob@secret.com')
  })

  // ── delete-parent-with-children TOMBSTONES ──────────────────────────────
  it('delete-parent-with-children tombstones (row kept, body blanked, PII nulled, child resolves parentId)', async () => {
    const parent = await post(
      db,
      { target, author: { kind: 'user', userId: 'consumer-u1' }, body: 'parent', initialStatus: 'published' },
      moderator,
      sanitize,
    )
    await post(
      db,
      { target, author: { kind: 'user', userId: 'consumer-u2' }, body: 'child', parentId: parent.id },
      { id: 'consumer-u2' },
      sanitize,
    )

    await remove(db, parent.id, author)
    const tombstone = await getById(db, parent.id)
    expect(tombstone).not.toBeNull()
    expect(tombstone!.body).toBe('')
    expect(tombstone!.bodyHtml).toBe('')
    expect(tombstone!.status).toBe('trashed')

    const childPage = await list(db, target, { status: 'pending' }, moderator)
    const child = childPage.items.find((c) => c.parentId === parent.id)
    expect(child).toBeDefined()
  })

  it('delete-leaf hard-deletes (no tombstone)', async () => {
    const ref = await post(
      db,
      { target, author: { kind: 'user', userId: 'consumer-u1' }, body: 'leaf', initialStatus: 'published' },
      moderator,
      sanitize,
    )
    await remove(db, ref.id, author)
    expect(await getById(db, ref.id)).toBeNull()
  })

  // ── body-length over MAX_BODY_LEN → CommentValidationError ─────────────
  it('body over MAX_BODY_LEN → CommentValidationError', async () => {
    await expect(
      post(
        db,
        { target, author: { kind: 'user', userId: 'u1' }, body: 'x'.repeat(10_001) },
        author,
        sanitize,
      ),
    ).rejects.toThrow(CommentValidationError)
  })

  // ── keyset pagination item parity ──────────────────────────────────────
  it('cursor-walked multi-page list deep-equals single limit=all list', async () => {
    // Insert 15 published comments (via moderator initialStatus)
    for (let i = 0; i < 15; i++) {
      await post(
        db,
        { target, author: { kind: 'user', userId: 'u1' }, body: `page-test ${i}`, initialStatus: 'published' },
        moderator,
        sanitize,
      )
    }

    const allPage = await list(db, target, { limit: 200, status: 'published' }, moderator)
    expect(allPage.items.length).toBe(15)

    const walked: typeof allPage.items = []
    let cursor: string | null = null
    do {
      const page = await list(
        db,
        target,
        { limit: 4, status: 'published', ...(cursor ? { cursor } : {}) },
        moderator,
      )
      walked.push(...page.items)
      cursor = page.nextCursor
    } while (cursor)

    // Deep item-parity (spec §4) — NOT just id/nextCursor equality.
    expect(walked).toEqual(allPage.items)
  })

  // ── isCommentError structural guard (no instanceof) ─────────────────────
  it('isCommentError structural guard works on errors from public barrel', async () => {
    try {
      await post(db, { target, author: { kind: 'user', userId: 'u1' }, body: '' }, author, sanitize)
    } catch (e) {
      expect(isCommentError(e)).toBe(true)
    }
  })

  // ── comments table drops clean (removability proof) ────────────────────
  it('comments table drops clean (forward migration is removable)', async () => {
    await db.execute(sql`DROP TABLE comments`)
    await expect(db.select().from(comments).limit(1)).rejects.toThrow(/comments|exist/i)
  })
})
