import { describe, expect, it } from 'vitest'
import {
  createAuth,
  getSession,
  hasPermission,
  InvalidSessionError,
  isAtLeastRole,
  PermissionDeniedError,
  requirePermission,
  requireRole,
  RoleDeniedError,
  type AuthEngine,
  type Principal,
} from './index.js'

function fakeEngine(overrides: Partial<AuthEngine> = {}): AuthEngine {
  return {
    signIn: async () => {
      throw new Error('not implemented')
    },
    signOut: async () => {},
    verifySession: async () => null,
    refresh: async () => null,
    createUser: async () => ({ userId: 'u1' }),
    setPassword: async () => {},
    verifyPassword: async () => false,
    ...overrides,
  }
}

describe('getSession', () => {
  it('returns null for missing headers without throwing', async () => {
    const engine = fakeEngine()
    await expect(getSession(new Headers(), engine)).resolves.toBeNull()
  })

  it('returns null for garbage cookie without throwing', async () => {
    const engine = fakeEngine()
    const headers = new Headers({ cookie: ';;;not-a-token=%%%' })
    await expect(getSession(headers, engine)).resolves.toBeNull()
  })

  it('returns principal from bearer token', async () => {
    const principal: Principal = {
      userId: 'u1',
      sessionId: 's1',
      roles: ['user'],
    }
    const engine = fakeEngine({
      verifySession: async (token) => (token === 'good' ? principal : null),
    })
    const headers = new Headers({ authorization: 'Bearer good' })
    await expect(getSession(headers, engine)).resolves.toEqual(principal)
  })

  it('returns principal from configured access cookie', async () => {
    const principal: Principal = {
      userId: 'u2',
      sessionId: 's2',
      roles: ['admin'],
    }
    const engine = fakeEngine({
      verifySession: async (token) => (token === 'cookie-tok' ? principal : null),
    })
    const headers = new Headers({ cookie: 'app_at=cookie-tok; other=x' })
    await expect(
      getSession(headers, engine, { accessCookieName: 'app_at' }),
    ).resolves.toEqual(principal)
  })
})

describe('createAuth', () => {
  it('delegates getSession to the engine', async () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: [] }
    const auth = createAuth(
      fakeEngine({
        verifySession: async () => principal,
      }),
    )
    const headers = new Headers({ authorization: 'Bearer x' })
    await expect(auth.getSession(headers)).resolves.toEqual(principal)
  })
})

describe('hasPermission', () => {
  it('returns false when capabilities are absent', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: [] }
    expect(hasPermission(principal, 'billing.read')).toBe(false)
  })

  it('returns true when capability is present', () => {
    const principal: Principal = {
      userId: 'u1',
      sessionId: 's1',
      roles: [],
      capabilities: ['billing.read'],
    }
    expect(hasPermission(principal, 'billing.read')).toBe(true)
  })
})

describe('isAtLeastRole', () => {
  const hierarchy = ['viewer', 'editor', 'admin'] as const

  it('returns false when principal role is below target', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: ['viewer'] }
    expect(isAtLeastRole(principal, 'admin', hierarchy)).toBe(false)
  })

  it('returns true when principal role meets or exceeds target', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: ['editor'] }
    expect(isAtLeastRole(principal, 'editor', hierarchy)).toBe(true)
    expect(isAtLeastRole(principal, 'viewer', hierarchy)).toBe(true)
  })
})

describe('requirePermission', () => {
  it('throws InvalidSessionError when principal is null', () => {
    const guard = requirePermission('posts.write')
    expect(() => guard(null)).toThrow(InvalidSessionError)
  })

  it('throws PermissionDeniedError when capability is missing', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: [] }
    const guard = requirePermission('posts.write')
    expect(() => guard(principal)).toThrow(PermissionDeniedError)
  })

  it('returns principal when capability is granted', () => {
    const principal: Principal = {
      userId: 'u1',
      sessionId: 's1',
      roles: [],
      capabilities: ['posts.write'],
    }
    const guard = requirePermission('posts.write')
    expect(guard(principal)).toBe(principal)
  })
})

describe('requireRole', () => {
  const hierarchy = ['viewer', 'editor', 'admin'] as const

  it('throws InvalidSessionError when principal is null', () => {
    const guard = requireRole('admin', hierarchy)
    expect(() => guard(null)).toThrow(InvalidSessionError)
  })

  it('throws RoleDeniedError when role is below target', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: ['editor'] }
    const guard = requireRole('admin', hierarchy)
    expect(() => guard(principal)).toThrow(RoleDeniedError)
  })

  it('carries the denied role on the error', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: ['viewer'] }
    const guard = requireRole('admin', hierarchy)
    try {
      guard(principal)
      expect.unreachable('should have thrown')
    } catch (e) {
      expect((e as RoleDeniedError).name).toBe('RoleDeniedError')
      expect((e as RoleDeniedError).role).toBe('admin')
    }
  })

  it('returns principal when role meets or exceeds target', () => {
    const principal: Principal = { userId: 'u1', sessionId: 's1', roles: ['admin'] }
    const guard = requireRole('admin', hierarchy)
    expect(guard(principal)).toBe(principal)
  })
})
