import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import type { AppEnv } from '../src/types'

const mockProvision = vi.fn()
const mockCleanup = vi.fn()
const mockHashPassword = vi.fn()
const mockHashToken = vi.fn()
const mockGenerateOpaqueToken = vi.fn()

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({ kind: 'db' })),
  provisionE2eRoleFixture: (...args: unknown[]) => mockProvision(...args),
  cleanupE2eRoleFixture: (...args: unknown[]) => mockCleanup(...args),
  E2eFixtureConflictError: class E2eFixtureConflictError extends Error {},
}))

vi.mock('@zync/auth', () => ({
  generateOpaqueToken: () => mockGenerateOpaqueToken(),
  hashPassword: (...args: unknown[]) => mockHashPassword(...args),
  hashToken: (...args: unknown[]) => mockHashToken(...args),
  timingSafeEqual: (left: string, right: string) => left === right,
}))

vi.mock('../src/lib/password-hash-do', () => ({
  withDoHash: vi.fn(() => ({ deriveBits: 'mock' })),
}))

import { e2eRoleFixturesRoute } from '../src/routes/e2e-role-fixtures'
import { E2eFixtureConflictError } from '@zync/db/queries'

const roles = ['OWNER', 'ADMIN', 'MEMBER', 'VIEWER', 'CONTRACTOR', 'ACCOUNTANT'] as const

function createApp() {
  const app = new Hono<AppEnv>()
  app.route('/api/e2e/role-fixtures', e2eRoleFixturesRoute)
  return app
}

function env(overrides: Record<string, unknown> = {}) {
  return {
    DB: { connectionString: 'postgresql://test:test@localhost/test' },
    ENVIRONMENT: 'preview',
    E2E_FIXTURE_SECRET: 'fixture-secret',
    ...overrides,
  } as unknown as AppEnv['Bindings']
}

const headers = {
  'content-type': 'application/json',
  'x-e2e-fixture-secret': 'fixture-secret',
}

describe('preview E2E role fixture lifecycle', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockGenerateOpaqueToken.mockReturnValue('generated-password')
    mockHashPassword.mockResolvedValue('password-hash')
    mockHashToken.mockImplementation(async (value: string) => `hash:${value}`)
    mockProvision.mockResolvedValue({
      tenantId: '00000000-0000-4000-8000-000000000001',
      tenantSlug: 'e2e-ci-42-abcd1234',
      users: roles.map((role, index) => ({
        id: `00000000-0000-4000-8000-00000000000${index + 2}`,
        email: `e2e+ci-42-${role.toLowerCase()}@zync.invalid`,
        role,
      })),
    })
    mockCleanup.mockResolvedValue(true)
  })

  it.each(['production', 'development', undefined])(
    'returns 404 outside preview (%s)',
    async (environment) => {
      const response = await createApp().request(
        '/api/e2e/role-fixtures',
        { method: 'POST', headers, body: JSON.stringify({ runId: 'ci-42' }) },
        env({ ENVIRONMENT: environment }),
      )

      expect(response.status).toBe(404)
      expect(mockProvision).not.toHaveBeenCalled()
      expect(mockHashPassword).not.toHaveBeenCalled()
    },
  )

  it.each([undefined, '', 'wrong-secret'])('rejects missing or wrong secret (%s)', async (secret) => {
    const requestHeaders = { ...headers, 'x-e2e-fixture-secret': secret ?? '' }
    const response = await createApp().request(
      '/api/e2e/role-fixtures',
      { method: 'POST', headers: requestHeaders, body: JSON.stringify({ runId: 'ci-42' }) },
      env(),
    )

    expect(response.status).toBe(403)
    expect(mockProvision).not.toHaveBeenCalled()
  })

  it.each(['', 'A', 'contains spaces', '../escape', 'x'.repeat(65)])(
    'rejects invalid run namespace %s',
    async (runId) => {
      const response = await createApp().request(
        '/api/e2e/role-fixtures',
        { method: 'POST', headers, body: JSON.stringify({ runId }) },
        env(),
      )

      expect(response.status).toBe(400)
      expect(mockProvision).not.toHaveBeenCalled()
    },
  )

  it('rejects caller-controlled roles', async () => {
    const response = await createApp().request(
      '/api/e2e/role-fixtures',
      {
        method: 'POST',
        headers,
        body: JSON.stringify({ runId: 'ci-42', roles: ['OWNER'] }),
      },
      env(),
    )

    expect(response.status).toBe(400)
    expect(mockProvision).not.toHaveBeenCalled()
  })

  it('provisions exactly the fixed role set with generated credentials', async () => {
    const response = await createApp().request(
      '/api/e2e/role-fixtures',
      { method: 'POST', headers, body: JSON.stringify({ runId: 'ci-42' }) },
      env(),
    )

    expect(response.status).toBe(201)
    expect(response.headers.get('cache-control')).toBe('no-store')
    const body = await response.json() as {
      runId: string
      tenantId: string
      users: Array<{ role: string; email: string; password: string }>
    }
    expect(body.runId).toBe('ci-42')
    expect(body.users.map(({ role }) => role)).toEqual(roles)
    expect(body.users.every(({ password }) => password === 'generated-password')).toBe(true)
    expect(mockHashPassword).toHaveBeenCalledWith('generated-password', { deriveBits: 'mock' })
    expect(mockProvision).toHaveBeenCalledWith(
      { kind: 'db' },
      { runId: 'ci-42', passwordHash: 'password-hash' },
    )
  })

  it('returns 409 when the run namespace already exists', async () => {
    mockProvision.mockRejectedValue(new E2eFixtureConflictError())

    const response = await createApp().request(
      '/api/e2e/role-fixtures',
      { method: 'POST', headers, body: JSON.stringify({ runId: 'ci-42' }) },
      env(),
    )

    expect(response.status).toBe(409)
  })

  it('cleans up the exact run namespace and all associated sessions', async () => {
    const response = await createApp().request(
      '/api/e2e/role-fixtures/ci-42',
      { method: 'DELETE', headers },
      env(),
    )

    expect(response.status).toBe(204)
    expect(mockCleanup).toHaveBeenCalledWith({ kind: 'db' }, 'ci-42')
  })

  it('returns 404 when cleanup finds no exact fixture', async () => {
    mockCleanup.mockResolvedValue(false)

    const response = await createApp().request(
      '/api/e2e/role-fixtures/ci-42',
      { method: 'DELETE', headers },
      env(),
    )

    expect(response.status).toBe(404)
  })

  it('maps a concurrent namespace conflict to 409 while the winner succeeds', async () => {
    mockProvision
      .mockResolvedValueOnce({
        tenantId: '00000000-0000-4000-8000-000000000001',
        tenantSlug: 'e2e-ci-42-abcd1234',
        users: roles.map((role) => ({ id: crypto.randomUUID(), email: `${role}@test`, role })),
      })
      .mockRejectedValueOnce(new E2eFixtureConflictError())

    const request = () => createApp().request(
      '/api/e2e/role-fixtures',
      { method: 'POST', headers, body: JSON.stringify({ runId: 'ci-42' }) },
      env(),
    )
    const responses = await Promise.all([request(), request()])

    expect(responses.map(({ status }) => status).sort()).toEqual([201, 409])
  })
})
