/**
 * sessionGuard + auth blocklist regression — A12a cluster.
 */
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Hono } from 'hono'
import { hashToken, sessionRevokedKey, signSession } from '@zync/auth'
import { TenantTier } from '@zync/types'
import type { AppEnv } from '../src/types'

const JWT_SECRET = 'test-jwt-secret'
const TENANT_ID = '00000000-0000-4000-8000-000000000001'
const USER_ID = '00000000-0000-4000-8000-000000000099'
const SESSION_ROW_ID = '00000000-0000-4000-8000-0000000000aa'
const TOKEN_HASH = 'abc123tokenhash'

const mockGetSessionByTokenHash = vi.fn()
const mockTouchSession = vi.fn()
const mockRevokeSession = vi.fn()
const mockGetTenantSecuritySettings = vi.fn()
const mockGetUserVersion = vi.fn()

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({})),
  getSessionByTokenHash: (...args: unknown[]) => mockGetSessionByTokenHash(...args),
  touchSession: (...args: unknown[]) => mockTouchSession(...args),
  revokeSession: (...args: unknown[]) => mockRevokeSession(...args),
  getTenantSecuritySettings: (...args: unknown[]) => mockGetTenantSecuritySettings(...args),
}))

vi.mock('../src/middleware/user-version', () => ({
  getUserVersion: (...args: unknown[]) => mockGetUserVersion(...args),
}))

import { authMiddleware } from '../src/middleware/auth'
import { sessionGuard } from '../src/middleware/session-guard'

function makeKvStore() {
  const store = new Map<string, string>()
  return {
    get: vi.fn(async (key: string) => store.get(key) ?? null),
    put: vi.fn(async (key: string, value: string) => {
      store.set(key, value)
    }),
    store,
  }
}

async function signedAccessToken() {
  return signSession(
    {
      sub: USER_ID,
      tid: TENANT_ID,
      role: 'OWNER',
      permissions: ['settings:read'],
      tier: TenantTier.FREELANCER,
      type: 'user',
      v: 1,
      enforce_2fa: false,
      two_factor_verified: false,
    },
    JWT_SECRET,
  )
}

function protectedApp(env: AppEnv['Bindings']) {
  const app = new Hono<AppEnv>()
  app.get('/protected', authMiddleware, (c) =>
    c.json({ ok: true, sessionId: c.get('sessionId') ?? null }),
  )
  return { app, env }
}

function executionContext() {
  const pending: Promise<unknown>[] = []
  return {
    pending,
    waitUntil(promise: Promise<unknown>) {
      pending.push(promise)
    },
  }
}

describe('authMiddleware blocklist + sessionGuard (A12a)', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGetUserVersion.mockResolvedValue(1)
    mockGetTenantSecuritySettings.mockResolvedValue({
      tenantId: TENANT_ID,
      idleTimeoutMinutes: null,
      maxSessionsPerUser: 10,
      require2faForRoles: [],
      blockSuspiciousLogins: false,
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    mockTouchSession.mockResolvedValue(undefined)
  })

  it('registers session touch with the request execution context', async () => {
    const ctx = executionContext()
    mockGetSessionByTokenHash.mockResolvedValue({
      id: SESSION_ROW_ID,
      userId: USER_ID,
      tenantId: TENANT_ID,
      revokedAt: null,
      lastActiveAt: new Date(),
    })

    const app = new Hono<AppEnv>()
    app.use('*', async (c, next) => {
      c.set('session', { type: 'user', tid: TENANT_ID, sub: USER_ID })
      c.set('accessTokenHash', TOKEN_HASH)
      c.set('db', {})
      await next()
    })
    app.use('*', sessionGuard)
    app.get('/protected', (c) => c.json({ ok: true }))
    const res = await app.request('/protected', {}, { DB: {}, JWT_SECRET, KV: {}, RATELIMIT_KV: {} } as never, ctx)

    expect(res.status).toBe(200)
    expect(ctx.pending).toHaveLength(1)
    await Promise.all(ctx.pending)
    expect(mockTouchSession).toHaveBeenCalledWith({}, TENANT_ID, SESSION_ROW_ID)
  })

  it('rejects legacy KV blocklist:{tokenHash} entries', async () => {
    const kv = makeKvStore()
    const ratelimitKv = makeKvStore()
    const token = await signedAccessToken()
    const tokenHash = await hashToken(token)
    kv.store.set(`blocklist:${tokenHash}`, '1')

    const { app, env } = protectedApp({
      DB: { connectionString: 'postgresql://test:test@localhost/test' },
      JWT_SECRET,
      KV: kv as unknown as KVNamespace,
      RATELIMIT_KV: ratelimitKv as unknown as KVNamespace,
    } as AppEnv['Bindings'])

    const res = await app.request('/protected', {
      headers: { Authorization: `Bearer ${token}` },
    }, env)

    expect(res.status).toBe(401)
    expect(mockGetSessionByTokenHash).not.toHaveBeenCalled()
  })

  it('rejects canonical session:revoked:{tokenHash} entries on RATELIMIT_KV', async () => {
    const kv = makeKvStore()
    const ratelimitKv = makeKvStore()
    const token = await signedAccessToken()
    const tokenHash = await hashToken(token)
    ratelimitKv.store.set(sessionRevokedKey(tokenHash), '1')

    const { app, env } = protectedApp({
      DB: { connectionString: 'postgresql://test:test@localhost/test' },
      JWT_SECRET,
      KV: kv as unknown as KVNamespace,
      RATELIMIT_KV: ratelimitKv as unknown as KVNamespace,
    } as AppEnv['Bindings'])

    const res = await app.request('/protected', {
      headers: { Authorization: `Bearer ${token}` },
    }, env)

    expect(res.status).toBe(401)
    expect(mockGetSessionByTokenHash).not.toHaveBeenCalled()
  })

  it('rejects tokens with no user_sessions row (fail-closed)', async () => {
    const kv = makeKvStore()
    const ratelimitKv = makeKvStore()
    const token = await signedAccessToken()
    mockGetSessionByTokenHash.mockResolvedValue(null)

    const { app, env } = protectedApp({
      DB: { connectionString: 'postgresql://test:test@localhost/test' },
      JWT_SECRET,
      KV: kv as unknown as KVNamespace,
      RATELIMIT_KV: ratelimitKv as unknown as KVNamespace,
    } as AppEnv['Bindings'])

    const res = await app.request('/protected', {
      headers: { Authorization: `Bearer ${token}` },
    }, env)

    expect(res.status).toBe(401)
  })

  it('sets sessionId and touches active sessions', async () => {
    const kv = makeKvStore()
    const ratelimitKv = makeKvStore()
    const token = await signedAccessToken()
    mockGetSessionByTokenHash.mockResolvedValue({
      id: SESSION_ROW_ID,
      tenantId: TENANT_ID,
      userId: USER_ID,
      tokenHash: TOKEN_HASH,
      lastActiveAt: new Date(),
      expiresAt: new Date(Date.now() + 3_600_000),
      revokedAt: null,
    })

    const { app, env } = protectedApp({
      DB: { connectionString: 'postgresql://test:test@localhost/test' },
      JWT_SECRET,
      KV: kv as unknown as KVNamespace,
      RATELIMIT_KV: ratelimitKv as unknown as KVNamespace,
    } as AppEnv['Bindings'])

    const res = await app.request('/protected', {
      headers: { Authorization: `Bearer ${token}` },
    }, env)

    expect(res.status).toBe(200)
    const body = (await res.json()) as { ok: boolean; sessionId: string }
    expect(body.sessionId).toBe(SESSION_ROW_ID)
    expect(mockTouchSession).toHaveBeenCalledWith({}, TENANT_ID, SESSION_ROW_ID)
  })

  it('idle timeout revokes, blocklists, and returns 401', async () => {
    const idleTenantId = '00000000-0000-4000-8000-000000000002'
    const kv = makeKvStore()
    const ratelimitKv = makeKvStore()
    const token = await signSession(
      {
        sub: USER_ID,
        tid: idleTenantId,
        role: 'OWNER',
        permissions: ['settings:read'],
        tier: TenantTier.FREELANCER,
        type: 'user',
        v: 1,
        enforce_2fa: false,
        two_factor_verified: false,
      },
      JWT_SECRET,
    )
    const expiresAt = new Date(Date.now() + 3_600_000)
    mockGetSessionByTokenHash.mockResolvedValue({
      id: SESSION_ROW_ID,
      tenantId: idleTenantId,
      userId: USER_ID,
      tokenHash: TOKEN_HASH,
      lastActiveAt: new Date(Date.now() - 31 * 60_000),
      expiresAt,
      revokedAt: null,
    })
    mockGetTenantSecuritySettings.mockResolvedValue({
      tenantId: idleTenantId,
      idleTimeoutMinutes: 30,
      maxSessionsPerUser: 10,
      require2faForRoles: [],
      blockSuspiciousLogins: false,
      createdAt: new Date(),
      updatedAt: new Date(),
    })
    mockRevokeSession.mockResolvedValue({
      tokens: [{ tokenHash: TOKEN_HASH, expiresAt }],
    })

    const { app, env } = protectedApp({
      DB: { connectionString: 'postgresql://test:test@localhost/test' },
      JWT_SECRET,
      KV: kv as unknown as KVNamespace,
      RATELIMIT_KV: ratelimitKv as unknown as KVNamespace,
    } as AppEnv['Bindings'])

    const res = await app.request('/protected', {
      headers: { Authorization: `Bearer ${token}` },
    }, env)

    expect(res.status).toBe(401)
    const body = (await res.json()) as { code: string }
    expect(body.code).toBe('idle_timeout')
    expect(mockRevokeSession).toHaveBeenCalledWith(
      {},
      SESSION_ROW_ID,
      USER_ID,
      idleTenantId,
      'idle_timeout',
    )

    await vi.waitFor(() => {
      expect(ratelimitKv.put).toHaveBeenCalled()
    })
    expect(ratelimitKv.store.has(sessionRevokedKey(TOKEN_HASH))).toBe(true)
  })
})
