import { sql, eq } from 'drizzle-orm'
import { describe, expect, it } from 'vitest'
import { createPgliteClient } from '@platform-modules/db/pglite'
import { hashToken } from '@platform-modules/util/tokens'
import {
  issueServiceToken,
  listServiceTokensForUser,
  resolveServiceToken,
  revokeServiceToken,
  revokeServiceTokensForUser,
} from './service-token.js'
import { customAuthSchema } from './schema.js'
import { CREATE_AUTH_USERS, CREATE_USER_SESSIONS, CREATE_SERVICE_TOKENS } from './test-ddl.js'

async function setupDb() {
  const db = createPgliteClient({ schema: customAuthSchema })
  await db.execute(CREATE_AUTH_USERS)
  await db.execute(CREATE_USER_SESSIONS)
  await db.execute(CREATE_SERVICE_TOKENS)
  return db
}

async function seedUser(db: Awaited<ReturnType<typeof setupDb>>, roles = '["admin"]') {
  const userId = 'user-1'
  await db.execute(sql`
    INSERT INTO auth_users (id, email, password_hash, session_version, roles, created_at)
    VALUES (${userId}, 'u@example.com', 'x', 0, ${roles}, NOW())
  `)
  return userId
}

describe('issueServiceToken', () => {
  it('mints an opaque token, stores ONLY its hash, returns the raw once', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const { id, token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'mcp' })
    expect(token).toMatch(/^[A-Za-z0-9_-]{40,}$/) // base64url opaque, not a JWT
    expect(token.split('.')).toHaveLength(1)       // NOT a 3-part JWT
    const [row] = await db.select().from(customAuthSchema.serviceTokens).where(eq(customAuthSchema.serviceTokens.id, id))
    expect(row).toBeDefined()
    expect(row!.tokenHash).toBe(await hashToken(token)) // stored = hash(raw)
    expect(row!.tokenHash).not.toBe(token)              // raw never stored
    expect(row!.status).toBe('active')
    expect(row!.expiresAt).toBeNull()                   // no ttl → non-expiring
  })

  it('honors ttlMs (sets expiresAt = now + ttl)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const now = new Date('2026-06-19T00:00:00Z')
    const { id } = await issueServiceToken(db, customAuthSchema, { userId, label: 'ci', ttlMs: 86_400_000, now })
    const [row] = await db.select().from(customAuthSchema.serviceTokens).where(eq(customAuthSchema.serviceTokens.id, id))
    expect(row).toBeDefined()
    expect(new Date(row!.expiresAt!).getTime()).toBe(now.getTime() + 86_400_000)
  })
})

describe('resolveServiceToken', () => {
  it('resolves an active token to a Principal with LIVE roles', async () => {
    const db = await setupDb()
    const userId = await seedUser(db, '["admin"]')
    const { id, token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'mcp' })
    const p = await resolveServiceToken(db, customAuthSchema, token)
    expect(p).not.toBeNull()
    expect(p!.userId).toBe(userId)
    expect(p!.roles).toEqual(['admin'])
    expect(p!.sessionId).toBe('svc:' + id) // synthetic, prefix-namespaced
  })

  it('reads roles LIVE (demotion takes effect immediately)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db, '["admin"]')
    const { token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'mcp' })
    await db.execute(sql`UPDATE auth_users SET roles = '["viewer"]' WHERE id = ${userId}`)
    const p = await resolveServiceToken(db, customAuthSchema, token)
    expect(p!.roles).toEqual(['viewer'])
  })

  it('returns null on unknown / revoked / expired (fail-closed)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    expect(await resolveServiceToken(db, customAuthSchema, 'nope-not-a-real-token')).toBeNull()

    const { id, token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'r' })
    await db.execute(sql`UPDATE service_tokens SET status = 'revoked' WHERE id = ${id}`)
    expect(await resolveServiceToken(db, customAuthSchema, token)).toBeNull()

    const past = new Date('2020-01-01T00:00:00Z')
    const { token: t2 } = await issueServiceToken(db, customAuthSchema, { userId, label: 'e', ttlMs: 1, now: past })
    expect(await resolveServiceToken(db, customAuthSchema, t2)).toBeNull()
  })

  it('does NOT couple to sessionVersion (a bump does not revoke the token)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const { token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'pat' })
    await db.execute(sql`UPDATE auth_users SET session_version = session_version + 1 WHERE id = ${userId}`)
    expect(await resolveServiceToken(db, customAuthSchema, token)).not.toBeNull()
  })

  it('returns null when the OWNER is disabled (fail-closed — PAT defense-in-depth)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const { token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'pat' })
    await db.execute(sql`UPDATE auth_users SET status = 'disabled' WHERE id = ${userId}`)
    expect(await resolveServiceToken(db, customAuthSchema, token)).toBeNull()
  })

  it('throttles lastUsedAt (does not rewrite on a second immediate resolve)', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const { id, token } = await issueServiceToken(db, customAuthSchema, { userId, label: 'u' })
    await resolveServiceToken(db, customAuthSchema, token)
    const [first] = await db.select().from(customAuthSchema.serviceTokens).where(eq(customAuthSchema.serviceTokens.id, id))
    expect(first).toBeDefined()
    expect(first!.lastUsedAt).not.toBeNull()
    await resolveServiceToken(db, customAuthSchema, token)
    const [second] = await db.select().from(customAuthSchema.serviceTokens).where(eq(customAuthSchema.serviceTokens.id, id))
    expect(second).toBeDefined()
    expect(new Date(second!.lastUsedAt!).getTime()).toBe(new Date(first!.lastUsedAt!).getTime()) // not rewritten
  })

  it('a resolve failure to write lastUsedAt MUST NOT change the verdict', async () => {
    // covered structurally: the update is wrapped so it cannot throw into the resolve path.
    // (No separate assertion needed beyond the happy path + revoked path above.)
  })
})

describe('revokeServiceToken / revokeServiceTokensForUser', () => {
  it('revokeServiceToken kills exactly that token, idempotently', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    const a = await issueServiceToken(db, customAuthSchema, { userId, label: 'a' })
    const b = await issueServiceToken(db, customAuthSchema, { userId, label: 'b' })
    await revokeServiceToken(db, customAuthSchema.serviceTokens, a.id)
    await revokeServiceToken(db, customAuthSchema.serviceTokens, a.id) // idempotent
    expect(await resolveServiceToken(db, customAuthSchema, a.token)).toBeNull()
    expect(await resolveServiceToken(db, customAuthSchema, b.token)).not.toBeNull()
  })

  it('revokeServiceTokensForUser kills all of one user, leaves others', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    await db.execute(sql`
      INSERT INTO auth_users (id, email, password_hash, session_version, roles, created_at)
      VALUES ('user-2', 'v@example.com', 'x', 0, '["admin"]', NOW())
    `)
    const a = await issueServiceToken(db, customAuthSchema, { userId, label: 'a' })
    const b = await issueServiceToken(db, customAuthSchema, { userId, label: 'b' })
    const other = await issueServiceToken(db, customAuthSchema, { userId: 'user-2', label: 'c' })
    await revokeServiceTokensForUser(db, customAuthSchema.serviceTokens, userId)
    expect(await resolveServiceToken(db, customAuthSchema, a.token)).toBeNull()
    expect(await resolveServiceToken(db, customAuthSchema, b.token)).toBeNull()
    expect(await resolveServiceToken(db, customAuthSchema, other.token)).not.toBeNull()
  })
})

describe('listServiceTokensForUser', () => {
  it('returns only active tokens for the requested user and never includes tokenHash', async () => {
    const db = await setupDb()
    const userId = await seedUser(db)
    await db.execute(sql`
      INSERT INTO auth_users (id, email, password_hash, session_version, roles, created_at)
      VALUES ('user-2', 'v@example.com', 'x', 0, '["admin"]', NOW())
    `)
    const first = await issueServiceToken(db, customAuthSchema, { userId, label: 'first' })
    const second = await issueServiceToken(db, customAuthSchema, { userId, label: 'second' })
    const other = await issueServiceToken(db, customAuthSchema, { userId: 'user-2', label: 'other' })
    await revokeServiceToken(db, customAuthSchema.serviceTokens, second.id)
    await db.execute(sql`UPDATE service_tokens SET last_used_at = NOW() WHERE id = ${first.id}`)
    await db.execute(sql`UPDATE service_tokens SET expires_at = NOW() + interval '1 day' WHERE id = ${first.id}`)
    await db.execute(sql`UPDATE service_tokens SET expires_at = NOW() + interval '1 day' WHERE id = ${other.id}`)

    const tokens = await listServiceTokensForUser(db, customAuthSchema, userId)

    expect(tokens).toHaveLength(1)
    expect(tokens[0]).toMatchObject({
      id: first.id,
      label: 'first',
      status: 'active',
    })
    expect(tokens[0]).not.toHaveProperty('tokenHash')
    expect(tokens.map((token) => token.id)).not.toContain(second.id)
    expect(tokens.map((token) => token.id)).not.toContain(other.id)
  })
})
