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

import type { AuthEngine, Principal } from '@platform-modules/auth'
import type { Querier } from '@platform-modules/db'
import { ok } from '../http.js'
import { requireAuth } from './auth.js'

const { getSessionMock, verifyApiKeyMock } = vi.hoisted(() => ({
  getSessionMock: vi.fn(),
  verifyApiKeyMock: vi.fn(),
}))

vi.mock('@platform-modules/auth', async () => {
  const actual = await vi.importActual<typeof import('@platform-modules/auth')>(
    '@platform-modules/auth',
  )

  return {
    ...actual,
    getSession: getSessionMock,
  }
})

vi.mock('@platform-modules/auth/api-keys', () => ({
  verifyApiKey: verifyApiKeyMock,
}))

type TestBindings = {}

function createAuthEngine(): AuthEngine {
  return {
    signIn: vi.fn(),
    signOut: vi.fn(),
    verifySession: vi.fn(),
    refresh: vi.fn(),
    createUser: vi.fn(),
    setPassword: vi.fn(),
    verifyPassword: vi.fn(),
  }
}

function createDb(): Querier<Record<string, never>> {
  return {} as Querier<Record<string, never>>
}

function createApp(options: Parameters<typeof requireAuth<TestBindings, Record<string, never>>>[0]) {
  const app = new Hono()

  app.use('*', requireAuth(options))
  app.get('/protected', (context) =>
    ok({
      principal: context.get('principal'),
      capabilities: context.get('capabilities'),
    }),
  )

  return app
}

async function readJson(response: Response): Promise<unknown> {
  return response.json()
}

describe('requireAuth', () => {
  beforeEach(() => {
    getSessionMock.mockReset()
    verifyApiKeyMock.mockReset()
  })

  it('resolves a bearer api key into a principal and capabilities', async () => {
    const tenancy = {
      resolveCapabilities: vi.fn(async () => ['plugin.read', 'plugin.write']),
    }
    const app = createApp({
      authEngine: createAuthEngine(),
      db: createDb(),
      tenancy,
      resolveApiKeyPrincipal: async (verified) => ({
        account: 'tenant-1',
        userId: verified.owner,
        scopes: verified.scopes,
      }),
    })

    verifyApiKeyMock.mockResolvedValue({
      owner: 'machine-user',
      scopes: ['plugin.read'],
    })

    const response = await app.request('http://press-zone.test/protected', {
      headers: { authorization: 'Bearer prefix.secret' },
    })

    expect(response.status).toBe(200)
    expect(getSessionMock).not.toHaveBeenCalled()
    expect(verifyApiKeyMock).toHaveBeenCalledTimes(1)
    expect(tenancy.resolveCapabilities).toHaveBeenCalledTimes(1)
    expect(tenancy.resolveCapabilities).toHaveBeenCalledWith('machine-user', 'tenant-1')
    await expect(readJson(response)).resolves.toEqual({
      data: {
        principal: {
          account: 'tenant-1',
          userId: 'machine-user',
          scopes: ['plugin.read'],
        },
        capabilities: ['plugin.read'],
      },
    })
  })

  it('returns a 401 envelope when the bearer api key is invalid', async () => {
    const tenancy = {
      resolveCapabilities: vi.fn(),
    }
    const app = createApp({
      authEngine: createAuthEngine(),
      db: createDb(),
      tenancy,
    })

    verifyApiKeyMock.mockResolvedValue(null)
    getSessionMock.mockResolvedValue({
      userId: 'user-1',
      sessionId: 'session-1',
      roles: ['user'],
    } satisfies Principal)

    const response = await app.request('http://press-zone.test/protected', {
      headers: {
        authorization: 'Bearer invalid.secret',
        cookie: 'access_token=session-token',
      },
    })

    expect(response.status).toBe(401)
    expect(getSessionMock).not.toHaveBeenCalled()
    expect(tenancy.resolveCapabilities).not.toHaveBeenCalled()
    await expect(readJson(response)).resolves.toEqual({
      error: {
        code: 'UNAUTHORIZED',
        message: 'Authentication required',
      },
    })
  })

  it('resolves a session principal, account, and capabilities from tenancy', async () => {
    const tenancy = {
      resolveCapabilities: vi.fn(async () => ['account.read', 'account.write']),
    }
    const app = createApp({
      authEngine: createAuthEngine(),
      db: createDb(),
      tenancy,
      resolveSessionAccount: async () => 'account-2',
    })

    getSessionMock.mockResolvedValue({
      userId: 'user-2',
      sessionId: 'session-2',
      roles: ['member'],
    } satisfies Principal)

    const response = await app.request('http://press-zone.test/protected', {
      headers: {
        cookie: 'access_token=session-token',
      },
    })

    expect(response.status).toBe(200)
    expect(verifyApiKeyMock).not.toHaveBeenCalled()
    expect(tenancy.resolveCapabilities).toHaveBeenCalledTimes(1)
    expect(tenancy.resolveCapabilities).toHaveBeenCalledWith('user-2', 'account-2')
    await expect(readJson(response)).resolves.toEqual({
      data: {
        principal: {
          account: 'account-2',
          userId: 'user-2',
          scopes: ['account.read', 'account.write'],
        },
        capabilities: ['account.read', 'account.write'],
      },
    })
  })
})
