/**
 * AGNTCY Conformance Test Suite for SentryAgent.ai AgentIdP.
 *
 * Verifies that the platform conforms to the AGNTCY agent identity specification:
 *   1. Agent registration creates a DID:WEB identifier.
 *   2. Token issuance for agent client (client_credentials grant).
 *   3. A2A delegation chain create + verify (gated by A2A_ENABLED).
 *   4. Compliance report generation returns a valid AGNTCY structure.
 */

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

// ── Environment setup — must happen before importing app ──────────────────────

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';
process.env['COMPLIANCE_ENABLED'] = 'true';

// Ensure A2A tests only run when the feature is on
const a2aEnabled = process.env['A2A_ENABLED'] !== 'false';

// ── Imports (after env is set) ────────────────────────────────────────────────

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

// ── Helpers ───────────────────────────────────────────────────────────────────

/**
 * Creates a signed JWT for use in test requests.
 *
 * @param sub - Subject (agentId).
 * @param scope - Space-separated OAuth 2.0 scopes.
 * @param organizationId - Optional organization_id claim.
 * @returns Signed JWT string.
 */
function makeToken(
  sub: string,
  scope: string = 'agents:read agents:write audit:read',
  organizationId?: string,
): string {
  const payload: Record<string, unknown> = { sub, client_id: sub, scope, jti: uuidv4() };
  if (organizationId !== undefined) {
    payload['organization_id'] = organizationId;
  }
  return signToken(payload as Parameters<typeof signToken>[0], privateKey);
}

// ── Test suite ────────────────────────────────────────────────────────────────

describe('AGNTCY Conformance Suite', () => {
  let app: Application;
  let pool: Pool;

  // ── Migrations required for conformance tests ─────────────────────────────

  const migrations = [
    `CREATE TABLE IF NOT EXISTS organizations (
      organization_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      name VARCHAR(255) NOT NULL UNIQUE,
      tier VARCHAR(32) NOT NULL DEFAULT 'free',
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    )`,
    `CREATE TABLE IF NOT EXISTS agents (
      agent_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      organization_id UUID REFERENCES organizations(organization_id),
      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',
      did VARCHAR(512),
      did_document JSONB,
      did_created_at TIMESTAMPTZ,
      is_public BOOLEAN NOT NULL DEFAULT false,
      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,
      vault_path VARCHAR(512),
      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,
      organization_id UUID,
      action VARCHAR(64) NOT NULL,
      outcome VARCHAR(16) NOT NULL,
      ip_address VARCHAR(64) NOT NULL DEFAULT '127.0.0.1',
      user_agent TEXT NOT NULL DEFAULT 'test',
      metadata JSONB NOT NULL DEFAULT '{}',
      hash VARCHAR(64) NOT NULL DEFAULT '',
      previous_hash VARCHAR(64) 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()
    )`,
    `CREATE TABLE IF NOT EXISTS agent_did_keys (
      key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      agent_id UUID NOT NULL REFERENCES agents(agent_id),
      key_type VARCHAR(32) NOT NULL,
      public_key TEXT NOT NULL,
      private_key_encrypted TEXT,
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    )`,
    `CREATE TABLE IF NOT EXISTS delegation_chains (
      chain_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      delegator_id UUID NOT NULL,
      delegatee_id UUID NOT NULL,
      scope TEXT NOT NULL,
      status VARCHAR(16) NOT NULL DEFAULT 'active',
      created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
      expires_at TIMESTAMPTZ,
      revoked_at TIMESTAMPTZ,
      token TEXT
    )`,
  ];

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

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

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

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

  // ── Conformance test 1: Agent registration creates DID:WEB identifier ─────

  describe('Conformance 1 — Agent registration creates DID:WEB identifier', () => {
    it('should register an agent and return a did field starting with did:web:', async () => {
      const agentId = uuidv4();
      const token = makeToken(agentId, 'agents:read agents:write');

      const res = await request(app)
        .post('/api/v1/agents')
        .set('Authorization', `Bearer ${token}`)
        .send({
          email: `conformance-agent-${agentId}@sentryagent.ai`,
          agentType: 'screener',
          version: '1.0.0',
          capabilities: ['identity:read'],
          owner: 'conformance-team',
          deploymentEnv: 'development',
        });

      expect(res.status).toBe(201);
      expect(res.body.agentId).toBeDefined();

      // Verify DID is present and conforms to did:web: scheme
      if (res.body.did !== undefined && res.body.did !== null) {
        expect(typeof res.body.did).toBe('string');
        expect((res.body.did as string).startsWith('did:web:')).toBe(true);
      }
    });
  });

  // ── Conformance test 2: Token issuance via client_credentials grant ────────

  describe('Conformance 2 — Token issuance for agent client (client_credentials)', () => {
    it('should issue a Bearer JWT via client_credentials grant', async () => {
      const agentId = uuidv4();
      const setupToken = makeToken(agentId, 'agents:read agents:write');

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

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

      expect(credRes.status).toBe(201);
      const { clientSecret } = credRes.body as { clientSecret: string };

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

      expect(tokenRes.status).toBe(200);
      expect(tokenRes.body.access_token).toBeDefined();
      expect(tokenRes.body.token_type).toBe('Bearer');
      expect(typeof tokenRes.body.access_token).toBe('string');

      // Verify JWT structure (3 parts separated by dots)
      const jwtParts = (tokenRes.body.access_token as string).split('.');
      expect(jwtParts).toHaveLength(3);
    });
  });

  // ── Conformance test 3: A2A delegation chain (gated by A2A_ENABLED) ────────

  (a2aEnabled ? describe : describe.skip)(
    'Conformance 3 — A2A delegation chain create + verify (A2A_ENABLED=true)',
    () => {
      it('should create and verify a delegation chain between two agents', async () => {
        const delegatorId = uuidv4();
        const delegateeId = uuidv4();

        // Insert both agents directly
        await pool.query(
          `INSERT INTO agents (agent_id, email, agent_type, version, capabilities, owner, deployment_env, status)
           VALUES
             ($1, $2, 'orchestrator', '1.0.0', '{"agents:delegate"}', 'delegator-team', 'development', 'active'),
             ($3, $4, 'screener', '1.0.0', '{"agents:read"}', 'delegatee-team', 'development', 'active')`,
          [
            delegatorId,
            `delegator-${delegatorId}@sentryagent.ai`,
            delegateeId,
            `delegatee-${delegateeId}@sentryagent.ai`,
          ],
        );

        const delegatorToken = makeToken(delegatorId, 'agents:read agents:write');

        // Create delegation chain
        const createRes = await request(app)
          .post('/api/v1/oauth2/token/delegate')
          .set('Authorization', `Bearer ${delegatorToken}`)
          .send({
            delegatee_id: delegateeId,
            scope: 'agents:read',
          });

        // Accept 201 (created) or 200 (already exists)
        expect([200, 201]).toContain(createRes.status);

        const delegationToken: string =
          createRes.body.token ??
          createRes.body.delegation_token ??
          createRes.body.access_token ??
          '';

        // Verify delegation chain if a token was returned
        if (delegationToken !== '') {
          const verifyRes = await request(app)
            .post('/api/v1/oauth2/token/verify-delegation')
            .set('Authorization', `Bearer ${delegatorToken}`)
            .send({ token: delegationToken });

          expect([200, 204]).toContain(verifyRes.status);
        }
      });
    },
  );

  // ── Conformance test 4: Compliance report returns valid AGNTCY structure ───

  describe('Conformance 4 — Compliance report returns valid AGNTCY structure', () => {
    it('should return a compliance report with all required AGNTCY fields', async () => {
      const orgId = uuidv4();
      const agentId = uuidv4();

      // Create organization and agent
      await pool.query(
        `INSERT INTO organizations (organization_id, name, tier) VALUES ($1, $2, 'free')`,
        [orgId, `conformance-org-${orgId}`],
      );
      await pool.query(
        `INSERT INTO agents (agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status)
         VALUES ($1, $2, $3, 'screener', '1.0.0', '{"identity:read"}', 'conformance-team', 'development', 'active')`,
        [agentId, orgId, `report-test-${agentId}@sentryagent.ai`],
      );

      const token = makeToken(agentId, 'agents:read audit:read', orgId);

      const res = await request(app)
        .get('/api/v1/compliance/report')
        .set('Authorization', `Bearer ${token}`);

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

      // Verify all required AGNTCY fields are present
      expect(res.body.generated_at).toBeDefined();
      expect(res.body.tenant_id).toBeDefined();
      expect(res.body.agntcy_schema_version).toBe('1.0');
      expect(res.body.sections).toBeInstanceOf(Array);
      expect(res.body.sections.length).toBeGreaterThan(0);
      expect(['pass', 'fail', 'warn']).toContain(res.body.overall_status);

      // Verify generated_at is a valid ISO 8601 string
      const generatedAt = new Date(res.body.generated_at as string);
      expect(generatedAt.getTime()).not.toBeNaN();

      // Verify each section has required fields
      for (const section of res.body.sections as Array<Record<string, unknown>>) {
        expect(typeof section['name']).toBe('string');
        expect(['pass', 'fail', 'warn']).toContain(section['status']);
        expect(typeof section['details']).toBe('string');
      }

      // Verify expected sections are present
      const sectionNames = (res.body.sections as Array<Record<string, unknown>>).map(
        (s) => s['name'],
      );
      expect(sectionNames).toContain('agent-identity');
      expect(sectionNames).toContain('audit-trail');
    });

    it('should return X-Cache: HIT on second request within cache window', async () => {
      const orgId = uuidv4();
      const agentId = uuidv4();

      await pool.query(
        `INSERT INTO organizations (organization_id, name, tier) VALUES ($1, $2, 'free')`,
        [orgId, `cache-test-org-${orgId}`],
      );
      await pool.query(
        `INSERT INTO agents (agent_id, organization_id, email, agent_type, version, capabilities, owner, deployment_env, status)
         VALUES ($1, $2, $3, 'screener', '1.0.0', '{}', 'cache-team', 'development', 'active')`,
        [agentId, orgId, `cache-test-${agentId}@sentryagent.ai`],
      );

      const token = makeToken(agentId, 'agents:read audit:read', orgId);

      // First request — populates cache
      await request(app)
        .get('/api/v1/compliance/report')
        .set('Authorization', `Bearer ${token}`);

      // Second request — should be served from cache
      const secondRes = await request(app)
        .get('/api/v1/compliance/report')
        .set('Authorization', `Bearer ${token}`);

      expect(secondRes.status).toBe(200);
      expect(secondRes.headers['x-cache']).toBe('HIT');
      expect(secondRes.body.from_cache).toBe(true);
    });
  });
});
