import { describe, it, expect, beforeAll } from 'vitest'
import { createPgliteClient } from '@platform-modules/db/pglite'
import { createCustomEngine, customAuthSchema, issueServiceToken, resolveServiceToken, revokeServiceToken } from '@platform-modules/auth/engine-custom'
import { InvalidSessionError, RateLimitedError, RoleDeniedError, type AuthEngine, type Principal, type RefreshResult } from '@platform-modules/auth'
import authSchemaSql from '../db/auth-schema.sql?raw'
import { resolveAdmin, resolveEditor, actorFromPrincipal, loginWithCredentials, logout } from './auth.js'
import { SESSION_COOKIE } from './admin-engine.js'

const SECRET = 'test-jwt-secret'
const PEPPER = { currentVersion: 'v1', secrets: { v1: 'test-pepper' } }

function makeFakeSink() {
  const sets: { name: string; value: string; opts: Record<string, unknown> }[] = []
  const deletes: { name: string; opts: Record<string, unknown> }[] = []
  return {
    sets,
    deletes,
    set(name: string, value: string, opts: Record<string, unknown>) { sets.push({ name, value, opts }) },
    delete(name: string, opts: Record<string, unknown>) { deletes.push({ name, opts }) },
  }
}

function stubEngine(refreshImpl: (rt: string) => Promise<RefreshResult | null>): AuthEngine {
  return {
    signIn: async () => { throw new Error('not used') },
    signOut: async () => {},
    verifySession: async () => null,
    refresh: refreshImpl,
    createUser: async () => ({ userId: 'x' }),
    setPassword: async () => {},
    verifyPassword: async () => false,
  }
}

const adminPrincipal: Principal = { userId: 'u1', sessionId: 's1', roles: ['admin'] }

async function seedAdminEngine() {
  const db = createPgliteClient({ schema: customAuthSchema })
  for (const stmt of authSchemaSql.split(';').map((s) => s.trim()).filter(Boolean)) {
    await db.execute(stmt)
  }
  const eng = createCustomEngine({ db, jwtSecrets: [SECRET], pepper: PEPPER })
  const email = 'admin@x.test'
  const password = 'pw-admin-123'
  await eng.createUser({ email, password, roles: ['admin'] })
  return { engine: eng, email, password }
}

let engine: AuthEngine
let adminToken: string
let viewerToken: string
let editorToken: string

beforeAll(async () => {
  const seeded = await seedAdminEngine()
  engine = seeded.engine
  await engine.createUser({ email: 'viewer@x.test', password: 'pw-viewer-123', roles: ['viewer'] })
  await engine.createUser({ email: 'editor@x.test', password: 'pw-editor-123', roles: ['editor'] })
  adminToken = (await engine.signIn({ email: 'admin@x.test', password: 'pw-admin-123' })).accessToken
  viewerToken = (await engine.signIn({ email: 'viewer@x.test', password: 'pw-viewer-123' })).accessToken
  editorToken = (await engine.signIn({ email: 'editor@x.test', password: 'pw-editor-123' })).accessToken
})

describe('resolveAdmin — unified auth identity, server-derived authMode', () => {
  it('accepts an auth-issued admin bearer and reports authMode=token', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x/api/admin/content', { headers: { authorization: `Bearer ${adminToken}` } })
    const { actor, authMode } = await resolveAdmin(req, engine, sink)
    expect(actor.canPublish).toBe(true)
    expect(actor.canEditAny).toBe(true)
    expect(authMode).toBe('token')
  })
  it('accepts an admin session cookie and reports authMode=session', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { cookie: `${SESSION_COOKIE}=${adminToken}` } })
    const { authMode } = await resolveAdmin(req, engine, sink)
    expect(authMode).toBe('session')
  })
  it('reads the cookie even when a junk Bearer coexists (isolated-headers, fail-closed-safe)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { authorization: 'Bearer not-a-jwt', cookie: `${SESSION_COOKIE}=${adminToken}` } })
    const { authMode } = await resolveAdmin(req, engine, sink)
    expect(authMode).toBe('session')
  })
  it('rejects a non-admin cookie with RoleDeniedError (403, valid session wrong role)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { cookie: `${SESSION_COOKIE}=${viewerToken}` } })
    await expect(resolveAdmin(req, engine, sink)).rejects.toBeInstanceOf(RoleDeniedError)
  })
  it('rejects an absent/invalid credential with InvalidSessionError (401)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { authorization: 'Bearer nope' } })
    await expect(resolveAdmin(req, engine, sink)).rejects.toBeInstanceOf(InvalidSessionError)
  })
})

describe('resolveAdmin — dual-cookie silent refresh', () => {
  it('refresh rotation sets both cookies', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => ({ principal: adminPrincipal, accessToken: 'newA', refreshToken: 'newR' }))
    const req = new Request('https://x/', { headers: { cookie: 'mod_refresh=oldR' } })
    const out = await resolveAdmin(req, engine, sink)
    expect(out.sessionId).toBe('s1')
    expect(sink.sets.map((s) => s.name).sort()).toEqual(['mod_refresh', 'mod_session'])
    const access = sink.sets.find((s) => s.name === 'mod_session')!
    expect(access.value).toBe('newA')
    expect(access.opts).toMatchObject({ httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
  })

  it('refresh grace sets only the access cookie', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => ({ principal: adminPrincipal, accessToken: 'newA' }))
    const req = new Request('https://x/', { headers: { cookie: 'mod_refresh=oldR' } })
    await resolveAdmin(req, engine, sink)
    expect(sink.sets.map((s) => s.name)).toEqual(['mod_session'])
  })

  it('failed refresh clears both cookies and throws', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => null)
    const req = new Request('https://x/', { headers: { cookie: 'mod_refresh=reused' } })
    await expect(resolveAdmin(req, engine, sink)).rejects.toMatchObject({ name: 'InvalidSessionError' })
    expect(sink.deletes.map((d) => d.name).sort()).toEqual(['mod_refresh', 'mod_session'])
    expect(sink.sets).toEqual([])
  })

  it('rate-limited refresh propagates RateLimitedError', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => { throw new RateLimitedError('rate limited', 'auth.refresh') })
    const req = new Request('https://x/', { headers: { cookie: 'mod_refresh=x' } })
    await expect(resolveAdmin(req, engine, sink)).rejects.toMatchObject({ name: 'RateLimitedError' })
  })

  it('no credential throws missing_principal', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => null)
    await expect(resolveAdmin(new Request('https://x/'), engine, sink)).rejects.toMatchObject({ name: 'InvalidSessionError' })
    expect(sink.sets).toEqual([])
    expect(sink.deletes).toEqual([])
  })

  it('non-admin refresh is role-denied by resolveAdmin', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => ({ principal: { userId: 'u2', sessionId: 's2', roles: ['editor'] }, accessToken: 'a' }))
    const req = new Request('https://x/', { headers: { cookie: 'mod_refresh=x' } })
    await expect(resolveAdmin(req, engine, sink)).rejects.toMatchObject({ name: 'RoleDeniedError' })
  })

  it('login returns the Principal and sets both cookies', async () => {
    const sink = makeFakeSink()
    const { engine, email, password } = await seedAdminEngine()
    const principal = await loginWithCredentials(engine, sink, { email, password })
    // AuthClient.signIn seam needs the full Principal back, not just { userId }.
    expect(principal.userId).toBeTruthy()
    expect(principal.sessionId).toBeTruthy()
    expect(principal.roles).toContain('admin')
    expect(sink.sets.map((s) => s.name).sort()).toEqual(['mod_refresh', 'mod_session'])
  })

  it('logout always clears cookies', async () => {
    const sink = makeFakeSink()
    const engine = stubEngine(async () => null)
    await logout(engine, sink, new Request('https://x/'), 'https://x').catch(() => {})
    expect(sink.deletes.map((d) => d.name).sort()).toEqual(['mod_refresh', 'mod_session'])
  })
})

describe('resolveAdmin — opaque service-token (PAT) Bearer fallback', () => {
  const TABLES = { authUsers: customAuthSchema.authUsers, serviceTokens: customAuthSchema.serviceTokens }
  let db: ReturnType<typeof createPgliteClient>
  let tokenEngine: AuthEngine
  let svcToken: string
  let svcId: string
  let resolveToken: (raw: string) => Promise<Principal | null>

  beforeAll(async () => {
    db = createPgliteClient({ schema: customAuthSchema })
    for (const stmt of authSchemaSql.split(';').map((s) => s.trim()).filter(Boolean)) {
      await db.execute(stmt)
    }
    tokenEngine = createCustomEngine({ db, jwtSecrets: [SECRET], pepper: PEPPER })
    const created = await tokenEngine.createUser({ email: 'agent@x.test', password: 'pw-agent-123', roles: ['admin'] })
    const issued = await issueServiceToken(db, TABLES, { userId: created.userId, label: 'mcp-ci' })
    svcToken = issued.token
    svcId = issued.id
    resolveToken = (raw) => resolveServiceToken(db, TABLES, raw)
  })

  it('accepts an opaque service token on the Bearer path and reports authMode=token', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x/api/admin/content', { headers: { authorization: `Bearer ${svcToken}` } })
    const { actor, authMode } = await resolveAdmin(req, tokenEngine, sink, resolveToken)
    expect(authMode).toBe('token')
    expect(actor.canPublish).toBe(true)
    expect(actor.canEditAny).toBe(true)
  })

  it('a junk opaque Bearer does NOT shadow a valid session cookie (fall-through to session)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { authorization: 'Bearer not-a-jwt-or-token', cookie: `${SESSION_COOKIE}=${adminToken}` } })
    const { authMode } = await resolveAdmin(req, engine, sink, resolveToken)
    expect(authMode).toBe('session')
  })

  it('rejects an unknown opaque Bearer fail-closed (InvalidSessionError, 401)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x/api/admin/content', { headers: { authorization: 'Bearer svc-unknown-garbage' } })
    await expect(resolveAdmin(req, tokenEngine, sink, resolveToken)).rejects.toBeInstanceOf(InvalidSessionError)
  })

  it('rejects a REVOKED service token (per-token revocation, not sv-coupled)', async () => {
    await revokeServiceToken(db, customAuthSchema.serviceTokens, svcId)
    const sink = makeFakeSink()
    const req = new Request('https://x/api/admin/content', { headers: { authorization: `Bearer ${svcToken}` } })
    await expect(resolveAdmin(req, tokenEngine, sink, resolveToken)).rejects.toBeInstanceOf(InvalidSessionError)
  })
})

describe('resolveEditor — min-editor route gate', () => {
  it('accepts an editor session and derives canPublish:false', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { cookie: `${SESSION_COOKIE}=${editorToken}` } })
    const { actor, authMode } = await resolveEditor(req, engine, sink)
    expect(actor.canEditAny).toBe(true)
    expect(actor.canPublish).toBe(false)
    expect(authMode).toBe('session')
  })

  it('accepts an admin session (hierarchy)', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { cookie: `${SESSION_COOKIE}=${adminToken}` } })
    const { actor } = await resolveEditor(req, engine, sink)
    expect(actor.canPublish).toBe(true)
  })

  it('rejects a viewer with RoleDeniedError', async () => {
    const sink = makeFakeSink()
    const req = new Request('https://x', { headers: { cookie: `${SESSION_COOKIE}=${viewerToken}` } })
    await expect(resolveEditor(req, engine, sink)).rejects.toBeInstanceOf(RoleDeniedError)
  })
})

describe('actorFromPrincipal — derives Actor from ROLES (engine fills roles, never capabilities)', () => {
  it('maps the admin role to full content rights', () => {
    const actor = actorFromPrincipal({ userId: 'u', sessionId: 's', roles: ['admin'] })
    expect(actor).toEqual({ id: 'u', canEditAny: true, canPublish: true })
  })

  it('maps the editor role to canEditAny without canPublish', () => {
    const actor = actorFromPrincipal({ userId: 'u', sessionId: 's', roles: ['editor'] })
    expect(actor).toEqual({ id: 'u', canEditAny: true, canPublish: false })
  })
})
