import { beforeEach, describe, expect, it, vi } from 'vitest'

const mockHashToken = vi.fn()
const mockVerifySession = vi.fn()
const mockIsTokenBlocklisted = vi.fn()
const mockBlocklistRevokedTokens = vi.fn()
const mockCreateDb = vi.fn()
const mockRevokeSession = vi.fn()
const mockTouchSession = vi.fn()
const mockEvaluateUserSessionRow = vi.fn()
const mockGetUserVersion = vi.fn()

vi.mock('@zync/auth', () => ({
  hashToken: (...args: unknown[]) => mockHashToken(...args),
  verifySession: (...args: unknown[]) => mockVerifySession(...args),
  isTokenBlocklisted: (...args: unknown[]) => mockIsTokenBlocklisted(...args),
  blocklistRevokedTokens: (...args: unknown[]) => mockBlocklistRevokedTokens(...args),
}))

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

vi.mock('../src/middleware/session-guard', () => ({
  evaluateUserSessionRow: (...args: unknown[]) => mockEvaluateUserSessionRow(...args),
}))

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

import { runSessionAuthentication } from '../src/lib/session-authentication'

const env = {
  KV: { get: vi.fn() },
  RATELIMIT_KV: {},
  JWT_SECRET: 'secret',
} as never

const session = { sub: 'user-1', tid: 'tenant-1', type: 'user', v: 0 }
const sessionRow = { id: 'session-1', userId: 'user-1', tenantId: 'tenant-1' }

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

describe('runSessionAuthentication', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockHashToken.mockResolvedValue('hash')
    mockVerifySession.mockResolvedValue(session)
    mockIsTokenBlocklisted.mockResolvedValue(false)
    mockGetUserVersion.mockResolvedValue(0)
    mockCreateDb.mockReturnValue({})
    mockEvaluateUserSessionRow.mockResolvedValue({ ok: true, sessionRow })
  })

  it('keeps valid authentication successful when session touch fails', async () => {
    mockTouchSession.mockRejectedValue(new Error('database unavailable'))

    await expect(runSessionAuthentication(env, 'token', executionContext())).resolves.toMatchObject({
      ok: true,
      sessionId: 'session-1',
    })
  })

  it('registers session touch with execution context and runs it', async () => {
    const ctx = executionContext()
    let touched = false
    let resolveTouch!: () => void
    mockTouchSession.mockReturnValue(new Promise<void>((resolve) => {
      resolveTouch = () => { touched = true; resolve() }
    }))

    await expect(runSessionAuthentication(env, 'token', ctx)).resolves.toMatchObject({ ok: true })
    expect(ctx.pending).toHaveLength(1)
    expect(touched).toBe(false)
    resolveTouch()
    await Promise.all(ctx.pending)
    expect(touched).toBe(true)
  })

  it('returns idle-timeout 401 immediately when cleanup fails', async () => {
    mockEvaluateUserSessionRow.mockResolvedValue({ ok: false, reason: 'idle_timeout', sessionRow })
    mockRevokeSession.mockRejectedValue(new Error('database unavailable'))

    await expect(runSessionAuthentication(env, 'token', executionContext())).resolves.toEqual({
      ok: false,
      status: 401,
      error: 'Session expired due to inactivity',
      code: 'idle_timeout',
    })
  })
})
