/**
 * Integration tests for W3C DID endpoints (Phase 3 Workstream 2).
 * Uses a real Postgres test DB and Redis test instance.
 *
 * Endpoints under test:
 *   GET /.well-known/did.json                    — unauthenticated
 *   GET /api/v1/agents/:agentId/did              — unauthenticated
 *   GET /api/v1/agents/:agentId/did/resolve      — requires auth + agents:read scope
 *   GET /api/v1/agents/:agentId/did/card         — unauthenticated
 */

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

// ─── Environment setup BEFORE app import ─────────────────────────────────────

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['DEFAULT_ORG_ID'] = 'org_system';
process.env['DID_WEB_DOMAIN'] = 'test.sentryagent.local';

// ─── App + utilities ──────────────────────────────────────────────────────────

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

const TEST_DOMAIN = 'test.sentryagent.local';
const CALLER_ID = uuidv4();

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

// ─── Suite ────────────────────────────────────────────────────────────────────

describe('DID Endpoints Integration Tests', () => {
  let app: Application;
  let pool: Pool;
  let agentId: string;

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

    // ── Create all required tables ──────────────────────────────────────────
    const migrations: string[] = [
      // tracking
      `CREATE TABLE IF NOT EXISTS schema_migrations (
        name VARCHAR(255) PRIMARY KEY,
        applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )`,

      // organizations (FK dependency for agents)
      `CREATE TABLE IF NOT EXISTS organizations (
        organization_id     VARCHAR(40)  PRIMARY KEY,
        name                VARCHAR(100) NOT NULL,
        slug                VARCHAR(50)  NOT NULL UNIQUE,
        plan_tier           VARCHAR(20)  NOT NULL DEFAULT 'free',
        max_agents          INTEGER      NOT NULL DEFAULT 100,
        max_tokens_per_month INTEGER     NOT NULL DEFAULT 10000,
        status              VARCHAR(20)  NOT NULL DEFAULT 'active',
        created_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
        updated_at          TIMESTAMPTZ  NOT NULL DEFAULT NOW()
      )`,

      // seed system org
      `INSERT INTO organizations
         (organization_id, name, slug, plan_tier, max_agents, max_tokens_per_month, status)
       VALUES
         ('org_system', 'System', 'system', 'enterprise', 999999, 999999999, 'active')
       ON CONFLICT (organization_id) DO NOTHING`,

      // agents (with DID columns added in migration 013)
      `CREATE TABLE IF NOT EXISTS agents (
        agent_id        UUID        PRIMARY KEY DEFAULT gen_random_uuid(),
        organization_id VARCHAR(40) NOT NULL DEFAULT 'org_system' 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             TEXT,
        did_created_at  TIMESTAMPTZ,
        created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
        updated_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW()
      )`,

      // credentials
      `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
      )`,

      // audit_events
      `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()
      )`,

      // token_revocations
      `CREATE TABLE IF NOT EXISTS token_revocations (
        jti        UUID        PRIMARY KEY,
        expires_at TIMESTAMPTZ NOT NULL,
        revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
      )`,

      // agent_did_keys (migration 012)
      `CREATE TABLE IF NOT EXISTS agent_did_keys (
        key_id          VARCHAR(40)  PRIMARY KEY,
        agent_id        UUID         NOT NULL REFERENCES agents(agent_id) ON DELETE CASCADE,
        organization_id VARCHAR(40)  NOT NULL,
        public_key_jwk  JSONB        NOT NULL,
        vault_key_path  TEXT         NOT NULL,
        key_type        VARCHAR(16)  NOT NULL DEFAULT 'EC',
        curve           VARCHAR(16)  NOT NULL DEFAULT 'P-256',
        created_at      TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
        rotated_at      TIMESTAMPTZ
      )`,
    ];

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

    // ── Seed a test agent with a DID and key ────────────────────────────────
    const agentResult = await pool.query<{ agent_id: string }>(
      `INSERT INTO agents
         (email, agent_type, version, capabilities, owner, deployment_env, status,
          did, did_created_at)
       VALUES
         ($1, 'orchestrator', '1.0.0', ARRAY['task-planning'], 'test-team', 'production', 'active',
          $2, NOW())
       RETURNING agent_id`,
      [
        `did-test-agent-${uuidv4()}@sentryagent.test`,
        `did:web:${TEST_DOMAIN}:agents:__PLACEHOLDER__`,
      ],
    );
    agentId = agentResult.rows[0].agent_id;

    // Update DID to use the real agent_id
    const did = `did:web:${TEST_DOMAIN}:agents:${agentId}`;
    await pool.query(`UPDATE agents SET did = $1 WHERE agent_id = $2`, [did, agentId]);

    // Insert a key for the agent
    await pool.query(
      `INSERT INTO agent_did_keys
         (key_id, agent_id, organization_id, public_key_jwk, vault_key_path, key_type, curve)
       VALUES
         ($1, $2, 'org_system', $3, 'dev:no-vault', 'EC', 'P-256')`,
      [
        `key_${uuidv4().replace(/-/g, '')}`,
        agentId,
        JSON.stringify({ kty: 'EC', crv: 'P-256', x: 'test_x', y: 'test_y' }),
      ],
    );
  });

  afterEach(async () => {
    // Clear Redis to ensure no stale cached documents between tests
    // We cannot easily flush Redis here without the client, so tests are designed to be order-independent
  });

  afterAll(async () => {
    await pool.query('DELETE FROM agent_did_keys');
    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 WHERE organization_id != 'org_system'`);
    await pool.end();
    await closePool();
    await closeRedisClient();
  });

  // ─── GET /.well-known/did.json ────────────────────────────────────────────

  describe('GET /.well-known/did.json', () => {
    it('should return 200 with a W3C DID Document for the instance', async () => {
      const res = await request(app).get('/.well-known/did.json');

      expect(res.status).toBe(200);
      expect(res.body['@context']).toContain('https://www.w3.org/ns/did/v1');
      expect(res.body.id).toBe(`did:web:${TEST_DOMAIN}`);
    });

    it('should not require authentication', async () => {
      const res = await request(app).get('/.well-known/did.json');
      expect(res.status).toBe(200);
    });

    it('should include a verificationMethod array', async () => {
      const res = await request(app).get('/.well-known/did.json');

      expect(res.body.verificationMethod).toBeInstanceOf(Array);
      expect(res.body.verificationMethod.length).toBeGreaterThan(0);
    });

    it('should include a service endpoint of type AgentIdentityProvider', async () => {
      const res = await request(app).get('/.well-known/did.json');

      expect(res.body.service).toBeInstanceOf(Array);
      const svc = res.body.service.find(
        (s: { type: string }) => s.type === 'AgentIdentityProvider',
      );
      expect(svc).toBeDefined();
    });

    it('should NEVER expose private key material', async () => {
      const res = await request(app).get('/.well-known/did.json');
      const body = JSON.stringify(res.body);
      expect(body).not.toContain('privateKeyPem');
      expect(body).not.toContain('PRIVATE KEY');
    });
  });

  // ─── GET /api/v1/agents/:agentId/did ─────────────────────────────────────

  describe('GET /api/v1/agents/:agentId/did', () => {
    it('should return 200 with the agent DID Document', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did`);

      expect(res.status).toBe(200);
      expect(res.body['@context']).toContain('https://www.w3.org/ns/did/v1');
      expect(res.body.id).toBe(`did:web:${TEST_DOMAIN}:agents:${agentId}`);
    });

    it('should not require authentication', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did`);
      expect(res.status).toBe(200);
    });

    it('should include AGNTCY extension fields', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did`);

      expect(res.body.agntcy).toBeDefined();
      expect(res.body.agntcy.agentId).toBe(agentId);
      expect(res.body.agntcy.agentType).toBe('orchestrator');
    });

    it('should return 404 for a non-existent agent', async () => {
      const nonExistentId = uuidv4();
      const res = await request(app).get(`/api/v1/agents/${nonExistentId}/did`);

      expect(res.status).toBe(404);
      expect(res.body.code).toBe('AGENT_NOT_FOUND');
    });

    it('should return 410 for a decommissioned agent', async () => {
      // Create a decommissioned agent
      const decommResult = await pool.query<{ agent_id: string }>(
        `INSERT INTO agents
           (email, agent_type, version, capabilities, owner, deployment_env, status)
         VALUES
           ($1, 'screener', '1.0.0', ARRAY['scan'], 'test-team', 'staging', 'decommissioned')
         RETURNING agent_id`,
        [`decommissioned-${uuidv4()}@sentryagent.test`],
      );
      const decommId = decommResult.rows[0].agent_id;

      const res = await request(app).get(`/api/v1/agents/${decommId}/did`);

      expect(res.status).toBe(410);
      expect(res.body.code).toBe('AGENT_DECOMMISSIONED');

      await pool.query('DELETE FROM agents WHERE agent_id = $1', [decommId]);
    });

    it('should NEVER expose private key material', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did`);
      const body = JSON.stringify(res.body);
      expect(body).not.toContain('privateKeyPem');
      expect(body).not.toContain('PRIVATE KEY');
    });
  });

  // ─── GET /api/v1/agents/:agentId/did/resolve ─────────────────────────────

  describe('GET /api/v1/agents/:agentId/did/resolve', () => {
    it('should return 200 with a W3C DID Resolution result when authenticated', async () => {
      const token = makeToken();
      const res = await request(app)
        .get(`/api/v1/agents/${agentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

      expect(res.status).toBe(200);
      expect(res.body.didDocument).toBeDefined();
      expect(res.body.didDocumentMetadata).toBeDefined();
      expect(res.body.didResolutionMetadata).toBeDefined();
    });

    it('should return 401 without authentication', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did/resolve`);
      expect(res.status).toBe(401);
    });

    it('should set Content-Type to application/ld+json with DID resolution profile', async () => {
      const token = makeToken();
      const res = await request(app)
        .get(`/api/v1/agents/${agentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

      expect(res.headers['content-type']).toContain('application/ld+json');
    });

    it('should include the DID document id in the resolution result', async () => {
      const token = makeToken();
      const res = await request(app)
        .get(`/api/v1/agents/${agentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

      expect(res.body.didDocument.id).toBe(`did:web:${TEST_DOMAIN}:agents:${agentId}`);
    });

    it('should return 404 for a non-existent agent', async () => {
      const token = makeToken();
      const nonExistentId = uuidv4();
      const res = await request(app)
        .get(`/api/v1/agents/${nonExistentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

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

    it('should include ISO timestamps in didDocumentMetadata', async () => {
      const token = makeToken();
      const res = await request(app)
        .get(`/api/v1/agents/${agentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

      expect(res.body.didDocumentMetadata.created).toMatch(/^\d{4}-\d{2}-\d{2}T/);
      expect(res.body.didDocumentMetadata.updated).toMatch(/^\d{4}-\d{2}-\d{2}T/);
    });

    it('should NEVER expose private key material', async () => {
      const token = makeToken();
      const res = await request(app)
        .get(`/api/v1/agents/${agentId}/did/resolve`)
        .set('Authorization', `Bearer ${token}`);

      const body = JSON.stringify(res.body);
      expect(body).not.toContain('privateKeyPem');
      expect(body).not.toContain('PRIVATE KEY');
    });
  });

  // ─── GET /api/v1/agents/:agentId/did/card ────────────────────────────────

  describe('GET /api/v1/agents/:agentId/did/card', () => {
    it('should return 200 with an AGNTCY agent card', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did/card`);

      expect(res.status).toBe(200);
      expect(res.body.did).toBe(`did:web:${TEST_DOMAIN}:agents:${agentId}`);
      expect(res.body.agentType).toBe('orchestrator');
      expect(res.body.capabilities).toEqual(['task-planning']);
      expect(res.body.owner).toBe('test-team');
      expect(res.body.identityProvider).toBe('https://idp.sentryagent.ai');
    });

    it('should not require authentication', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did/card`);
      expect(res.status).toBe(200);
    });

    it('should include a valid ISO issuedAt timestamp', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did/card`);

      expect(res.body.issuedAt).toBeDefined();
      expect(() => new Date(res.body.issuedAt as string)).not.toThrow();
      expect(new Date(res.body.issuedAt as string).toISOString()).toBe(res.body.issuedAt);
    });

    it('should return 404 for a non-existent agent', async () => {
      const nonExistentId = uuidv4();
      const res = await request(app).get(`/api/v1/agents/${nonExistentId}/did/card`);

      expect(res.status).toBe(404);
      expect(res.body.code).toBe('AGENT_NOT_FOUND');
    });

    it('should NEVER expose private key material', async () => {
      const res = await request(app).get(`/api/v1/agents/${agentId}/did/card`);
      const body = JSON.stringify(res.body);
      expect(body).not.toContain('privateKeyPem');
      expect(body).not.toContain('PRIVATE KEY');
    });
  });
});
