/**
 * Integration tests for OAuth2 Token Service endpoints.
 */

import crypto from 'crypto';
import request from 'supertest';
import { Application } from 'express';
import { v4 as uuidv4 } from 'uuid';
import { Pool } from 'pg';

const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048,
  publicKeyEncoding: { type: 'spki', format: 'pem' },
  privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});

process.env['DATABASE_URL'] = process.env['TEST_DATABASE_URL'] ?? 'postgresql://sentryagent:sentryagent@localhost:5432/sentryagent_idp_test';
process.env['REDIS_URL'] = process.env['TEST_REDIS_URL'] ?? 'redis://localhost:6379/1';
process.env['JWT_PRIVATE_KEY'] = privateKey;
process.env['JWT_PUBLIC_KEY'] = publicKey;
process.env['NODE_ENV'] = 'test';

import { createApp } from '../../src/app';
import { signToken } from '../../src/utils/jwt';
import { closePool } from '../../src/db/pool';
import { closeRedisClient } from '../../src/cache/redis';

function makeToken(sub: string, scope: string = 'agents:read tokens:read'): string {
  return signToken({ sub, client_id: sub, scope, jti: uuidv4() }, privateKey);
}

describe('OAuth2 Token Service Integration Tests', () => {
  let app: Application;
  let pool: Pool;

  beforeAll(async () => {
    app = await createApp();
    pool = new Pool({ connectionString: process.env['DATABASE_URL'] });

    const migrations = [
      `CREATE TABLE IF NOT EXISTS agents (
        agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        email VARCHAR(255) NOT NULL UNIQUE,
        agent_type VARCHAR(32) NOT NULL,
        version VARCHAR(64) NOT NULL,
        capabilities TEXT[] NOT NULL DEFAULT '{}',
        owner VARCHAR(128) NOT NULL,
        deployment_env VARCHAR(16) NOT NULL,
        status VARCHAR(24) NOT NULL DEFAULT 'active',
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )`,
      `CREATE TABLE IF NOT EXISTS credentials (
        credential_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        client_id UUID NOT NULL,
        secret_hash VARCHAR(255) NOT NULL,
        status VARCHAR(16) NOT NULL DEFAULT 'active',
        created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        expires_at TIMESTAMPTZ,
        revoked_at TIMESTAMPTZ
      )`,
      `CREATE TABLE IF NOT EXISTS audit_events (
        event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        agent_id UUID NOT NULL,
        action VARCHAR(32) NOT NULL,
        outcome VARCHAR(16) NOT NULL,
        ip_address VARCHAR(64) NOT NULL,
        user_agent TEXT NOT NULL,
        metadata JSONB NOT NULL DEFAULT '{}',
        timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )`,
      `CREATE TABLE IF NOT EXISTS token_revocations (
        jti UUID PRIMARY KEY,
        expires_at TIMESTAMPTZ NOT NULL,
        revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )`,
    ];

    for (const sql of migrations) {
      await pool.query(sql);
    }
  });

  afterEach(async () => {
    await pool.query('DELETE FROM audit_events');
    await pool.query('DELETE FROM token_revocations');
    await pool.query('DELETE FROM credentials');
    await pool.query('DELETE FROM agents');
  });

  afterAll(async () => {
    await pool.end();
    await closePool();
    await closeRedisClient();
  });

  async function createAgentWithCredentials(): Promise<{ agentId: string; clientSecret: string }> {
    const agentId = uuidv4();
    const token = makeToken(agentId, 'agents:read agents:write tokens:read');

    // Create agent directly in DB
    await pool.query(
      `INSERT INTO agents (agent_id, email, agent_type, version, capabilities, owner, deployment_env, status)
       VALUES ($1, $2, 'screener', '1.0.0', '{"agents:read"}', 'test', 'development', 'active')`,
      [agentId, `agent-${agentId}@test.ai`],
    );

    // Generate credentials via API
    const credRes = await request(app)
      .post(`/api/v1/agents/${agentId}/credentials`)
      .set('Authorization', `Bearer ${token}`)
      .send({});

    return { agentId, clientSecret: credRes.body.clientSecret };
  }

  describe('POST /api/v1/token', () => {
    it('should issue a token for valid credentials', async () => {
      const { agentId, clientSecret } = await createAgentWithCredentials();

      const res = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({
          grant_type: 'client_credentials',
          client_id: agentId,
          client_secret: clientSecret,
          scope: 'agents:read',
        });

      expect(res.status).toBe(200);
      expect(res.body.access_token).toBeDefined();
      expect(res.body.token_type).toBe('Bearer');
      expect(res.body.expires_in).toBe(3600);
      expect(res.headers['cache-control']).toBe('no-store');
    });

    it('should return 400 for missing grant_type', async () => {
      const res = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({ client_id: uuidv4(), client_secret: 'secret' });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('invalid_request');
    });

    it('should return 400 for unsupported grant_type', async () => {
      const res = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({ grant_type: 'authorization_code' });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('unsupported_grant_type');
    });

    it('should return 401 for invalid credentials', async () => {
      const res = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({
          grant_type: 'client_credentials',
          client_id: uuidv4(),
          client_secret: 'wrong-secret',
          scope: 'agents:read',
        });

      expect(res.status).toBe(401);
      expect(res.body.error).toBe('invalid_client');
    });

    it('should return 400 for invalid scope', async () => {
      const res = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({
          grant_type: 'client_credentials',
          client_id: uuidv4(),
          client_secret: 'secret',
          scope: 'admin:all',
        });

      expect(res.status).toBe(400);
      expect(res.body.error).toBe('invalid_scope');
    });
  });

  describe('POST /api/v1/token/introspect', () => {
    it('should return active:true for a valid token', async () => {
      const { agentId, clientSecret } = await createAgentWithCredentials();
      const scope = 'agents:read tokens:read';

      const issued = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({ grant_type: 'client_credentials', client_id: agentId, client_secret: clientSecret, scope });

      const callerToken = issued.body.access_token;

      const res = await request(app)
        .post('/api/v1/token/introspect')
        .set('Authorization', `Bearer ${callerToken}`)
        .type('form')
        .send({ token: callerToken });

      expect(res.status).toBe(200);
      expect(res.body.active).toBe(true);
    });

    it('should return active:false for an invalid token', async () => {
      const callerToken = makeToken(uuidv4(), 'tokens:read');

      const res = await request(app)
        .post('/api/v1/token/introspect')
        .set('Authorization', `Bearer ${callerToken}`)
        .type('form')
        .send({ token: 'not.a.real.token' });

      expect(res.status).toBe(200);
      expect(res.body.active).toBe(false);
    });

    it('should return 401 without Bearer token', async () => {
      const res = await request(app)
        .post('/api/v1/token/introspect')
        .type('form')
        .send({ token: 'some.token' });
      expect(res.status).toBe(401);
    });
  });

  describe('POST /api/v1/token/revoke', () => {
    it('should revoke a token and return 200', async () => {
      const { agentId, clientSecret } = await createAgentWithCredentials();

      const issued = await request(app)
        .post('/api/v1/token')
        .type('form')
        .send({ grant_type: 'client_credentials', client_id: agentId, client_secret: clientSecret, scope: 'agents:read' });

      const token = issued.body.access_token;

      const res = await request(app)
        .post('/api/v1/token/revoke')
        .set('Authorization', `Bearer ${token}`)
        .type('form')
        .send({ token });

      expect(res.status).toBe(200);
    });

    it('should return 401 without Bearer token', async () => {
      const res = await request(app)
        .post('/api/v1/token/revoke')
        .type('form')
        .send({ token: 'some.token' });
      expect(res.status).toBe(401);
    });
  });
});
