import Fastify from 'fastify';
import bcrypt from 'bcryptjs';
import { beforeEach, describe, expect, it } from 'vitest';
import { MemoryDatastoreProvider } from '@bytelyst/datastore';
import { setProvider } from './datastore.js';
import { registerOptionalApiKeyContext, requireJwtOrApiKey } from './api-key-auth.js';
import { clearAll as clearRateLimits } from '../modules/ratelimit/store.js';

const rawApiKey = `wai_${'a'.repeat(64)}`;
const secondRawApiKey = `wai_${'b'.repeat(64)}`;
let provider: MemoryDatastoreProvider;

async function seedApiKeyToken(
  scopes: string[] = ['jobs:read'],
  overrides: Partial<{
    id: string;
    rawKey: string;
    userId: string;
    userName: string;
    tokenType: 'user_api' | 'product_api' | 'service_api';
    environment: 'dev' | 'staging' | 'prod';
    productId: string;
  }> = {}
) {
  const collection = provider.getCollection('api_tokens', '/id');
  await collection.create({
    id: overrides.id ?? 'tok_api_1',
    productId: overrides.productId ?? 'lysnrai',
    userId: overrides.userId ?? 'svc_jobs',
    userName: overrides.userName ?? 'Jobs Service',
    tokenType: overrides.tokenType ?? 'service_api',
    environment: overrides.environment ?? 'dev',
    prefix: (overrides.rawKey ?? rawApiKey).slice(0, 12),
    tokenHash: await bcrypt.hash(overrides.rawKey ?? rawApiKey, 10),
    status: 'active',
    scopes,
    expiresAt: '2099-01-01T00:00:00.000Z',
    lastUsed: null,
  });
}

describe('api key auth', () => {
  beforeEach(async () => {
    provider = new MemoryDatastoreProvider();
    setProvider(provider);
    delete process.env.API_KEY_RATE_LIMIT_CONFIG_JSON;
    delete process.env.API_KEY_PRODUCT_RATE_LIMIT_CONFIG_JSON;
    delete process.env.PLATFORM_RUNTIME_ENV;
    await clearRateLimits();
  });

  it('attaches apiKeyAuth from x-api-key', async () => {
    await seedApiKeyToken();
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      const actor = await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:read'],
        rateLimitKey: 'jobs:read',
      });
      return { actor, apiKeyAuth: req.apiKeyAuth };
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
      },
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toMatchObject({
      actor: {
        actorId: 'svc_jobs',
        productId: 'lysnrai',
        source: 'api_key',
      },
      apiKeyAuth: {
        tokenType: 'service_api',
        environment: 'dev',
      },
    });
  });

  it('rejects user_api tokens for machine authentication', async () => {
    await seedApiKeyToken(['jobs:read'], { tokenType: 'user_api' });
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:read'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
      },
    });

    expect(res.statusCode).toBe(401);
  });

  it('rejects api keys issued for another environment', async () => {
    process.env.PLATFORM_RUNTIME_ENV = 'prod';
    await seedApiKeyToken(['jobs:read'], { environment: 'staging' });
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:read'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
      },
    });

    expect(res.statusCode).toBe(401);
  });

  it('rejects api key without required scopes', async () => {
    await seedApiKeyToken(['jobs:read']);
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:write'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
      },
    });

    expect(res.statusCode).toBe(403);
    const auditDocs = await provider.getCollection('audit_log', '/productId').findMany({
      filter: { productId: 'lysnrai', action: 'api_key.access_rejected' },
    });
    expect(auditDocs).toHaveLength(1);
    expect(auditDocs[0]?.details).toMatchObject({
      event: 'missing_scope',
    });
  });

  it('rejects api key when token type is not permitted for the route', async () => {
    await seedApiKeyToken(['maintenance:read'], { tokenType: 'product_api' });
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['maintenance:read'],
        apiKeyTokenTypes: ['service_api'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
      },
    });

    expect(res.statusCode).toBe(403);
    expect(JSON.parse(res.body).error).toBe('Forbidden');
  });

  it('rejects api key when x-product-id targets another product', async () => {
    await seedApiKeyToken(['jobs:read']);
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:read'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': rawApiKey,
        'x-product-id': 'chronomind',
      },
    });

    expect(res.statusCode).toBe(403);
    expect(JSON.parse(res.body).error).toBe('Forbidden');
  });

  it('rate limits per api key and action key', async () => {
    process.env.API_KEY_RATE_LIMIT_CONFIG_JSON = JSON.stringify({
      'jobs:write': {
        maxRequests: 1,
        windowSeconds: 60,
      },
    });

    await seedApiKeyToken(['jobs:write']);
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.post('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:write'],
        rateLimitKey: 'jobs:write',
      });
    });

    const first = await app.inject({
      method: 'POST',
      url: '/probe',
      headers: { 'x-api-key': rawApiKey },
    });
    expect(first.statusCode).toBe(200);

    const second = await app.inject({
      method: 'POST',
      url: '/probe',
      headers: { 'x-api-key': rawApiKey },
    });
    expect(second.statusCode).toBe(429);
  });

  it('rate limits across api keys for the same product and action key', async () => {
    process.env.API_KEY_RATE_LIMIT_CONFIG_JSON = JSON.stringify({
      'jobs:write': {
        maxRequests: 5,
        windowSeconds: 60,
      },
    });
    process.env.API_KEY_PRODUCT_RATE_LIMIT_CONFIG_JSON = JSON.stringify({
      'jobs:write': {
        maxRequests: 1,
        windowSeconds: 60,
      },
    });

    await seedApiKeyToken(['jobs:write'], {
      id: 'tok_api_1',
      rawKey: rawApiKey,
      userId: 'svc_jobs_primary',
      userName: 'Jobs Primary',
    });
    await seedApiKeyToken(['jobs:write'], {
      id: 'tok_api_2',
      rawKey: secondRawApiKey,
      userId: 'svc_jobs_secondary',
      userName: 'Jobs Secondary',
    });

    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.post('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:write'],
        rateLimitKey: 'jobs:write',
      });
    });

    const first = await app.inject({
      method: 'POST',
      url: '/probe',
      headers: { 'x-api-key': rawApiKey },
    });
    expect(first.statusCode).toBe(200);

    const second = await app.inject({
      method: 'POST',
      url: '/probe',
      headers: { 'x-api-key': secondRawApiKey },
    });
    expect(second.statusCode).toBe(429);
  });

  it('allows JWT callers when configured', async () => {
    const app = Fastify();
    await registerOptionalApiKeyContext(app);
    app.addHook('onRequest', async req => {
      req.jwtPayload = {
        sub: 'admin_1',
        role: 'admin',
        productId: 'lysnrai',
      };
    });

    app.get('/probe', async req => {
      return await requireJwtOrApiKey(req, {
        jwtRoles: ['admin'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
    });

    expect(res.statusCode).toBe(200);
    expect(JSON.parse(res.body)).toMatchObject({
      actorId: 'admin_1',
      source: 'jwt',
    });
  });

  it('audits invalid api key attempts before auth context is established', async () => {
    const app = Fastify();
    await registerOptionalApiKeyContext(app);

    app.get('/probe', async req => {
      return requireJwtOrApiKey(req, {
        apiKeyScopes: ['jobs:read'],
      });
    });

    const res = await app.inject({
      method: 'GET',
      url: '/probe',
      headers: {
        'x-api-key': `wai_${'z'.repeat(64)}`,
      },
    });

    expect(res.statusCode).toBe(401);
    const auditDocs = await provider.getCollection('audit_log', '/productId').findMany({
      filter: { productId: 'lysnrai', action: 'api_key.access_rejected' },
    });
    expect(auditDocs.some(doc => doc.details?.event === 'invalid_api_key')).toBe(true);
  });
});
