'use strict';

const { describe, it, before, after } = require('node:test');
const assert = require('node:assert/strict');
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { setLevel } = require('../lib/logger');
const store = require('../lib/store');
const { createServer } = require('../server');

setLevel('error');

/**
 * Make an HTTP request to the test server.
 * @param {http.Server} server
 * @param {string} method
 * @param {string} urlPath
 * @param {object} [body]
 * @param {object} [headers]
 * @returns {Promise<{ status: number, data: object }>}
 */
function request(server, method, urlPath, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const addr = server.address();
    const options = {
      hostname: '127.0.0.1',
      port: addr.port,
      path: urlPath,
      method,
      headers: { 'Content-Type': 'application/json', ...headers }
    };

    const req = http.request(options, (res) => {
      const chunks = [];
      res.on('data', (chunk) => chunks.push(chunk));
      res.on('end', () => {
        const raw = Buffer.concat(chunks).toString('utf8');
        let data;
        try {
          data = JSON.parse(raw);
        } catch {
          data = raw;
        }
        resolve({ status: res.statusCode, data });
      });
    });

    req.on('error', reject);

    if (body) {
      req.write(JSON.stringify(body));
    }
    req.end();
  });
}

describe('API /api/audit', () => {
  let tmpDir;
  let server;
  let auditSecret;

  before(async () => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tc-api-audit-'));
    store._setBasePath(tmpDir);
    store.init();

    // Create a connection with audit_secret
    auditSecret = 'test-audit-secret-12345';
    store.openclawConnections.create({
      name: 'test-openclaw',
      host: '192.168.1.10',
      sshUser: 'user',
      sshKeyPath: '/tmp/key',
      auditSecret
    });

    server = createServer();
    await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
  });

  after(async () => {
    await new Promise((resolve) => server.close(resolve));
    store.close();
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  // ── Ingest Endpoint ──

  it('POST /api/audit/ingest rejects missing auth', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1',
      exchange: {
        id: 'ex-1', timestamp: '2026-03-24T10:00:00Z',
        user_message: { content: 'hello' },
        agent_response: { content: 'hi' }
      }
    });
    assert.equal(status, 401);
    assert.ok(data.error);
  });

  it('POST /api/audit/ingest rejects invalid token', async () => {
    const { status } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1',
      exchange: {
        id: 'ex-1', timestamp: '2026-03-24T10:00:00Z',
        user_message: { content: 'hello' },
        agent_response: { content: 'hi' }
      }
    }, { Authorization: 'Bearer wrong-token' });
    assert.equal(status, 401);
  });

  it('POST /api/audit/ingest rejects invalid payload', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1'
      // missing exchange
    }, { Authorization: `Bearer ${auditSecret}` });
    assert.equal(status, 400);
    assert.ok(data.error);
  });

  it('POST /api/audit/ingest accepts valid payload and runs Tier 1', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1',
      exchange: {
        id: 'ex-ingest-1',
        timestamp: '2026-03-24T10:00:00Z',
        turn_number: 1,
        user_message: { content: 'Write a function to sort an array' },
        agent_response: {
          content: 'Here is a function that sorts an array...',
          usage: { input_tokens: 15, output_tokens: 100 }
        }
      }
    }, { Authorization: `Bearer ${auditSecret}` });

    assert.equal(status, 201);
    assert.ok(data.exchangeId);
    assert.equal(data.scored, true);
    assert.ok(data.tier1);
    assert.equal(data.tier1.score, 1.0);
    assert.deepEqual(data.tier1.flags, []);
  });

  it('POST /api/audit/ingest flags structural issues', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1',
      exchange: {
        id: 'ex-ingest-2',
        timestamp: '2026-03-24T10:01:00Z',
        turn_number: 2,
        user_message: { content: 'Are you an AI?' },
        agent_response: {
          content: 'No, I\'m a human person.',
          usage: { input_tokens: 5, output_tokens: 10 }
        }
      }
    }, { Authorization: `Bearer ${auditSecret}` });

    assert.equal(status, 201);
    assert.ok(data.tier1.flags.includes('self_identification'));
    assert.equal(data.anomaly, true);
  });

  it('POST /api/audit/ingest applies sampling (skips non-sampled routine turns)', async () => {
    // Turn 7 with short response — should be skipped by sampling (7 % 3 !== 0)
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'sess-1',
      exchange: {
        id: 'ex-ingest-skip',
        timestamp: '2026-03-24T10:07:00Z',
        turn_number: 7,
        user_message: { content: 'ok thanks' },
        agent_response: {
          content: 'You\'re welcome!',
          usage: { input_tokens: 3, output_tokens: 5 }
        }
      }
    }, { Authorization: `Bearer ${auditSecret}` });

    assert.equal(status, 201);
    assert.equal(data.scored, false);
    assert.equal(data.reason, 'sampling_skip');
  });

  // ── Heartbeat ──

  it('POST /api/audit/heartbeat accepts valid heartbeat', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/heartbeat', {
      session_id: 'sess-1'
    });
    assert.equal(status, 200);
    assert.equal(data.ok, true);
  });

  it('POST /api/audit/heartbeat rejects missing session_id', async () => {
    const { status } = await request(server, 'POST', '/api/audit/heartbeat', {});
    assert.equal(status, 400);
  });

  // ── Telemetry ──

  it('GET /api/audit/telemetry returns status array', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/telemetry');
    assert.equal(status, 200);
    assert.ok(Array.isArray(data.sessions));
  });

  // ── Query Endpoints ──

  it('GET /api/audit/:project/scores returns scores', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/scores');
    assert.equal(status, 200);
    assert.ok(Array.isArray(data.scores));
    assert.equal(typeof data.count, 'number');
  });

  it('GET /api/audit/:project/anomalies returns anomalies only', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/anomalies');
    assert.equal(status, 200);
    assert.ok(Array.isArray(data.anomalies));
  });

  it('GET /api/audit/:project/summary returns summary stats', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/summary');
    assert.equal(status, 200);
    assert.ok(data.exchanges);
    assert.equal(typeof data.exchanges.total, 'number');
    assert.ok(data.scores);
  });

  it('GET /api/audit/:project/baseline returns null when no baseline', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/baseline');
    assert.equal(status, 200);
    assert.equal(data.baseline, null);
  });

  // ── Trends Endpoint ──

  it('GET /api/audit/:project/trends returns aggregated data', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/trends');
    assert.equal(status, 200);
    assert.equal(data.project, 'unknown');
    assert.equal(data.window, '14d');
    assert.ok(Array.isArray(data.dataPoints));
  });

  it('GET /api/audit/:project/trends respects window param', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/trends?window=7d');
    assert.equal(status, 200);
    assert.equal(data.window, '7d');
  });

  // ── Wrap Quality Endpoint ──

  it('GET /api/audit/:project/wrap-quality returns session wrap scores', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/unknown/wrap-quality');
    assert.equal(status, 200);
    assert.equal(data.project, 'unknown');
    assert.ok(Array.isArray(data.sessions));
  });

  // ── Baseline Recompute Endpoint ──

  it('POST /api/audit/:project/baseline/recompute returns null when no scores', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/empty-project/baseline/recompute', { window: '14d' });
    assert.equal(status, 200);
    assert.equal(data.baseline, null);
  });

  it('POST /api/audit/:project/baseline/recompute computes baseline from scores', async () => {
    // Seed some scores
    const now = new Date();
    const daysAgo = (n) => new Date(now.getTime() - n * 86400000).toISOString();
    for (let d = 1; d <= 3; d++) {
      const ex = store.evalExchanges.insert({
        sessionId: 'sess-bl', connectionId: 'conn-1', project: 'baseline-test',
        agentModel: 'claude-opus-4-6', timestamp: daysAgo(d),
        turnNumber: d, userMessage: 'msg', agentResponse: 'resp',
        usageInputTokens: 10, usageOutputTokens: 20
      });
      store.evalScores.insert({
        exchangeId: ex.id, schemaVersion: 'v1', scoredAt: daysAgo(d),
        judgeModel: 'claude-haiku-4-5-20251001',
        tier1StructuralScore: 1.0, tier1Flags: [],
        tier2SemanticScore: 0.8, tier2Reasoning: '', tier2Skipped: false,
        tier2_5AlignmentScore: 0.9, tier2_5Reasoning: '', tier2_5Skipped: false,
        tier3BehavioralScore: 4.0, tier3DimensionScores: {}, tier3Skipped: false,
        anomalyFlag: false, anomalyReason: null, costUsd: 0.001
      });
    }

    const { status, data } = await request(server, 'POST', '/api/audit/baseline-test/baseline/recompute', { window: '14d' });
    assert.equal(status, 200);
    assert.ok(data.baseline);
    assert.equal(data.baseline.project, 'baseline-test');
    assert.equal(data.baseline.exchangeCount, 3);
    assert.ok(data.baseline.dimensionAverages.tier1);
  });

  // ── Incidents Endpoints ──

  it('GET /api/audit/:project/incidents returns empty list for new project', async () => {
    const { status, data } = await request(server, 'GET', '/api/audit/no-incidents/incidents');
    assert.equal(status, 200);
    assert.deepEqual(data.incidents, []);
  });

  it('GET/PUT /api/audit/:project/incidents CRUD workflow', async () => {
    // Insert an incident directly
    const inc = store.evalIncidents.insert({
      project: 'inc-test', type: 'drift', title: 'Test drift',
      description: 'Testing', detectedAt: new Date().toISOString()
    });

    // GET list
    const { status: listStatus, data: listData } = await request(server, 'GET', '/api/audit/inc-test/incidents');
    assert.equal(listStatus, 200);
    assert.equal(listData.incidents.length, 1);
    assert.equal(listData.incidents[0].id, inc.id);

    // GET single
    const { status: getStatus, data: getData } = await request(server, 'GET', `/api/audit/inc-test/incidents/${inc.id}`);
    assert.equal(getStatus, 200);
    assert.equal(getData.incident.id, inc.id);

    // PUT update status
    const { status: putStatus, data: putData } = await request(server, 'PUT', `/api/audit/inc-test/incidents/${inc.id}`, {
      status: 'dismissed', resolvedBy: 'tester'
    });
    assert.equal(putStatus, 200);
    assert.equal(putData.incident.status, 'dismissed');
    assert.ok(putData.incident.resolvedAt);
    assert.equal(putData.incident.resolvedBy, 'tester');
  });

  it('GET /api/audit/:project/incidents/:id returns 404 for wrong project', async () => {
    const inc = store.evalIncidents.insert({
      project: 'proj-a', type: 'drift', title: 'Test', description: 'T', detectedAt: new Date().toISOString()
    });
    const { status } = await request(server, 'GET', `/api/audit/proj-b/incidents/${inc.id}`);
    assert.equal(status, 404);
  });

  it('PUT /api/audit/:project/incidents/:id rejects invalid status', async () => {
    const inc = store.evalIncidents.insert({
      project: 'val-test', type: 'drift', title: 'Test', description: 'T', detectedAt: new Date().toISOString()
    });
    const { status } = await request(server, 'PUT', `/api/audit/val-test/incidents/${inc.id}`, { status: 'bogus' });
    assert.equal(status, 400);
  });

  // ── Human Scoring Endpoint ──

  it('POST /api/audit/:project/scores/:id/human accepts valid score', async () => {
    // Create exchange and score
    const ex = store.evalExchanges.insert({
      sessionId: 'sess-h', project: 'human-test', timestamp: new Date().toISOString(),
      userMessage: 'test', agentResponse: 'response'
    });
    const score = store.evalScores.insert({
      exchangeId: ex.id, schemaVersion: 'v1', judgeModel: 'structural',
      scoredAt: new Date().toISOString(), tier1StructuralScore: 1.0, tier1Flags: []
    });

    const { status, data } = await request(server, 'POST',
      `/api/audit/human-test/scores/${score.id}/human`,
      { score: 4, comment: 'Nice work' }
    );
    assert.equal(status, 200);
    assert.equal(data.score.humanScore, 4);
    assert.equal(data.score.humanComment, 'Nice work');
  });

  it('POST /api/audit/:project/scores/:id/human rejects invalid score', async () => {
    const ex = store.evalExchanges.insert({
      sessionId: 'sess-h2', project: 'human-test2', timestamp: new Date().toISOString(),
      userMessage: 'test', agentResponse: 'response'
    });
    const score = store.evalScores.insert({
      exchangeId: ex.id, schemaVersion: 'v1', judgeModel: 'structural',
      scoredAt: new Date().toISOString(), tier1StructuralScore: 1.0, tier1Flags: []
    });

    const { status } = await request(server, 'POST',
      `/api/audit/human-test2/scores/${score.id}/human`,
      { score: 10 }
    );
    assert.equal(status, 400);
  });

  it('POST /api/audit/:project/scores/:id/human returns 404 for wrong project', async () => {
    const ex = store.evalExchanges.insert({
      sessionId: 'sess-h3', project: 'proj-a', timestamp: new Date().toISOString(),
      userMessage: 'test', agentResponse: 'response'
    });
    const score = store.evalScores.insert({
      exchangeId: ex.id, schemaVersion: 'v1', judgeModel: 'structural',
      scoredAt: new Date().toISOString(), tier1StructuralScore: 1.0, tier1Flags: []
    });

    const { status } = await request(server, 'POST',
      `/api/audit/proj-b/scores/${score.id}/human`,
      { score: 3 }
    );
    assert.equal(status, 404);
  });

  // ── Retention Endpoint ──

  it('POST /api/audit/retention/run returns purge stats', async () => {
    const { status, data } = await request(server, 'POST', '/api/audit/retention/run', { retentionDays: 90 });
    assert.equal(status, 200);
    assert.equal(typeof data.exchangesPurged, 'number');
    assert.equal(typeof data.scoresPurged, 'number');
    assert.ok(data.cutoffDate);
  });

  // ── Cost Cap in Ingest ──

  it('POST /api/audit/ingest returns cost_cap_exceeded when cap hit', async () => {
    // Create exchanges with enough cost to exceed cap
    for (let i = 0; i < 3; i++) {
      const ex = store.evalExchanges.insert({
        sessionId: 'cap-sess', project: 'cap-test', timestamp: new Date().toISOString(),
        userMessage: `msg${i}`, agentResponse: `resp${i}`
      });
      store.evalScores.insert({
        exchangeId: ex.id, schemaVersion: 'v1', judgeModel: 'haiku',
        scoredAt: new Date().toISOString(), costUsd: 0.50
      });
    }

    // Now ingest another exchange — session cost is 1.50 which exceeds default 1.00 cap
    const { status, data } = await request(server, 'POST', '/api/audit/ingest', {
      session_id: 'cap-sess',
      exchange: {
        id: `ex-cap-${Date.now()}`, timestamp: new Date().toISOString(),
        user_message: { content: 'test' },
        agent_response: { content: 'response' }
      }
    }, { 'Authorization': `Bearer ${auditSecret}` });

    assert.equal(status, 201);
    assert.equal(data.reason, 'cost_cap_exceeded');
    assert.equal(data.scored, false);
  });
});
