/**
 * Smoke tests for the enhanced error-logs route (DC-052).
 *
 * Mirrors `caddy-upstreams.routes.test.js`: build the router with stubbed
 * deps, hit it via a tiny express app, assert the response shape and
 * the audit-logger interactions.
 *
 * Fixture: a synthetic error log with two ERR entries and one WARN entry,
 * each with a different context, IP, and stack — enough to exercise the
 * filter chain (level, context, search, since/until) without pulling the
 * real 47k-line error.log off the host.
 */

const express = require('express');
const path = require('path');
const fs = require('fs');
const os = require('os');

const ENTRY_SEP = '='.repeat(80);
const FIXTURE_LOG = [
  `[2026-08-17T10:00:00.000Z] [ERR] updater: getLocalVersion failed: no candidate package.json found`,
  `    at SelfUpdater.getLocalVersion (/app/src/docker/self-updater.js:128:9)`,
  `  request: GET /api/v1/updates/check | ip: 100.85.236.10 | ua: Mozilla/5.0 | id: a1`,
  `  context: {"triggeredBy":"manual"}`,
  ENTRY_SEP,
  `[2026-08-17T11:00:00.000Z] [ERR] http: GET /api/v1/templates 503`,
  `    at Logger.error (/app/src/utils/logging.js:258:49)`,
  `  request: GET /api/v1/templates | ip: 100.85.236.11 | ua: curl/8.0 | id: a2`,
  `  context: {"service":"templates"}`,
  ENTRY_SEP,
  `[2026-08-17T12:00:00.000Z] [WARN] ssl-monitor: cert check failed: TLS connect error for sonarr.sami:443: getaddrinfo ENOTFOUND sonarr.sami`,
  `    at Logger.warn (/app/src/utils/logging.js:200:10)`,
  `  request: GET /api/v1/ssl/check | ip: 100.121.150.22 | ua: NodeHealthCheck | id: a3`,
  `  context: {"service":"sonarr"}`,
  ENTRY_SEP,
  ``,
].join('\n');

function buildFakeAuditLogger() {
  return {
    clear: jest.fn(async () => {}),
    log: jest.fn(async () => {}),
  };
}

function writeFixtureLog(tmpDir) {
  const logFile = path.join(tmpDir, 'error.log');
  fs.writeFileSync(logFile, FIXTURE_LOG);
  return logFile;
}

describe('routes/errorlogs (DC-052)', () => {
  let tmpDir;
  let logFile;
  let auditLogger;

  beforeEach(() => {
    tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dc052-errorlogs-'));
    logFile = writeFixtureLog(tmpDir);
    auditLogger = buildFakeAuditLogger();
  });

  afterEach(() => {
    fs.rmSync(tmpDir, { recursive: true, force: true });
  });

  function buildRouter() {
    const mod = require('../../routes/errorlogs');
    return mod({
      ERROR_LOG_FILE: logFile,
      auditLogger,
      asyncHandler: (fn) => async (req, res, next) => {
        try { await fn(req, res, next); } catch (e) { next(e); }
      },
    });
  }

  function listen(router) {
    const app = express();
    app.use(express.json());
    app.use(router);
    return app.listen(0);
  }

  test('router exposes the DC-052 endpoints', () => {
    const router = buildRouter();
    const paths = router.stack
      .filter((l) => l.route)
      .map((l) => Object.keys(l.route.methods).map((m) => `${m.toUpperCase()} ${l.route.path}`))
      .flat();
    expect(paths).toEqual(expect.arrayContaining([
      'GET /error-logs',
      'GET /error-logs/contexts',
      'DELETE /error-logs',
    ]));
  });

  test('GET /error-logs returns newest-first with totals', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs`);
    const body = await res.json();
    server.close();
    expect(res.status).toBe(200);
    expect(body.success).toBe(true);
    expect(body.total).toBe(3);
    expect(body.logs).toHaveLength(3);
    expect(body.hasMore).toBe(false);
    expect(body.filters).toEqual({
      level: null, context: null, search: null, since: null, until: null,
    });
    // Newest first: 12:00 (WARN ssl-monitor) → 11:00 (ERR http) → 10:00 (ERR updater).
    expect(body.logs[0].level).toBe('WARN');
    expect(body.logs[1].level).toBe('ERR');
    expect(body.logs[2].level).toBe('ERR');
    expect(body.logs[0].timestamp).toBe('2026-08-17T12:00:00.000Z');
  });

  test('GET /error-logs filters by level', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?level=ERR`);
    const body = await res.json();
    server.close();
    expect(body.total).toBe(2);
    expect(body.logs.every((e) => e.level === 'ERR')).toBe(true);
  });

  test('GET /error-logs filters by context (substring)', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?context=updater`);
    const body = await res.json();
    server.close();
    expect(body.total).toBe(1);
    expect(body.logs[0].context).toBe('updater');
  });

  test('GET /error-logs free-text search hits error / context / detail', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    // "sonarr" appears only in the WARN stack; should still match via detail.
    let res = await fetch(`http://127.0.0.1:${port}/error-logs?search=sonarr`);
    let body = await res.json();
    expect(body.total).toBe(1);
    expect(body.logs[0].context).toBe('ssl-monitor');
    // "503" appears only in the ERR http message; should match via error.
    res = await fetch(`http://127.0.0.1:${port}/error-logs?search=503`);
    body = await res.json();
    expect(body.total).toBe(1);
    expect(body.logs[0].context).toBe('http');
    server.close();
  });

  test('GET /error-logs respects since/until as numeric ISO compare', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    // Window covers only 11:00Z entry.
    const res = await fetch(
      `http://127.0.0.1:${port}/error-logs?since=${encodeURIComponent('2026-08-17T10:30:00Z')}&until=${encodeURIComponent('2026-08-17T11:30:00Z')}`
    );
    const body = await res.json();
    server.close();
    expect(body.total).toBe(1);
    expect(body.logs[0].timestamp).toBe('2026-08-17T11:00:00.000Z');
  });

  test('GET /error-logs rejects invalid since with 400', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?since=not-a-date`);
    const body = await res.json();
    server.close();
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
  });

  test('GET /error-logs rejects unknown level with 400', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?level=FATAL`);
    const body = await res.json();
    server.close();
    expect(res.status).toBe(400);
  });

  test('GET /error-logs paginates and reports hasMore', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res1 = await fetch(`http://127.0.0.1:${port}/error-logs?limit=2&offset=0`);
    const body1 = await res1.json();
    expect(body1.logs).toHaveLength(2);
    expect(body1.total).toBe(3);
    expect(body1.hasMore).toBe(true);
    const res2 = await fetch(`http://127.0.0.1:${port}/error-logs?limit=2&offset=2`);
    const body2 = await res2.json();
    expect(body2.logs).toHaveLength(1);
    expect(body2.hasMore).toBe(false);
    server.close();
  });

  test('GET /error-logs clamps limit to MAX_LIMIT', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?limit=99999`);
    const body = await res.json();
    server.close();
    // 3 entries total so we still get 3, but the route didn't blow up on a
    // giant limit; the contract is limit <= 500 and we just clamp.
    expect(body.logs.length).toBeLessThanOrEqual(500);
    expect(body.total).toBe(3);
  });

  test('GET /error-logs/contexts returns distinct contexts sorted by count', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs/contexts`);
    const body = await res.json();
    server.close();
    expect(body.success).toBe(true);
    expect(body.contexts).toHaveLength(3);
    // updater + http + ssl-monitor — each appears once.
    const names = body.contexts.map((c) => c.name).sort();
    expect(names).toEqual(['http', 'ssl-monitor', 'updater']);
    expect(body.contexts.every((c) => c.count === 1)).toBe(true);
  });

  test('DELETE /error-logs without confirm is rejected with 400', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs`, { method: 'DELETE' });
    const body = await res.json();
    server.close();
    expect(res.status).toBe(400);
    expect(body.success).toBe(false);
    // File still intact.
    expect(fs.readFileSync(logFile, 'utf8')).toBe(FIXTURE_LOG);
  });

  test('DELETE /error-logs with confirm=CLEAR truncates and audits', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs`, {
      method: 'DELETE',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ confirm: 'CLEAR' }),
    });
    const body = await res.json();
    server.close();
    expect(res.status).toBe(200);
    expect(body.success).toBe(true);
    expect(fs.readFileSync(logFile, 'utf8')).toBe('');
    expect(auditLogger.log).toHaveBeenCalledWith(expect.objectContaining({
      action: 'error-log.clear',
      outcome: 'success',
    }));
  });

  test('GET /error-logs returns empty when log file missing', async () => {
    fs.unlinkSync(logFile);
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs`);
    const body = await res.json();
    server.close();
    expect(body.success).toBe(true);
    expect(body.logs).toEqual([]);
    expect(body.total).toBe(0);
  });

  test('GET /error-logs preserves stack frames in detail field', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?context=updater`);
    const body = await res.json();
    server.close();
    expect(body.logs[0].detail).toContain('self-updater.js:128');
    expect(body.logs[0].detail).toContain('context:');
  });

  test('GET /error-logs handles malformed entry as raw fallback', async () => {
    // The parser splits the log on ENTRY_SEP (80 equal signs). A junk block
    // that has no timestamp header should still surface as a raw entry so
    // the operator doesn't lose forensic context. Place the malformed
    // block AFTER the separator so it ends up in its own split segment.
    fs.writeFileSync(logFile, [
      `[2026-08-17T13:00:00.000Z] [ERR] junk: well-formed entry`,
      ENTRY_SEP,
      `this is a malformed block with no timestamp header`,
      `and no level bracket at all`,
      ENTRY_SEP,
      ``,
    ].join('\n'));
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs`);
    const body = await res.json();
    server.close();
    expect(body.total).toBe(2);
    const raw = body.logs.find((e) => e.level === null);
    expect(raw).toBeDefined();
    expect(raw.error).toContain('malformed block');
    expect(raw.raw).toContain('malformed block');
  });

  test('GET /error-logs/contexts returns empty array when file missing', async () => {
    fs.unlinkSync(logFile);
    const server = listen(buildRouter());
    const { port } = server.address();
    const res = await fetch(`http://127.0.0.1:${port}/error-logs/contexts`);
    const body = await res.json();
    server.close();
    expect(body.success).toBe(true);
    expect(body.contexts).toEqual([]);
  });

  test('GET /error-logs?search matches IP field', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    // 100.85.236.11 is only on the /api/v1/templates entry.
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?search=100.85.236.11`);
    const body = await res.json();
    server.close();
    expect(body.total).toBe(1);
    expect(body.logs[0].request.ip).toBe('100.85.236.11');
  });

  test('GET /error-logs accepts huge since/until without error', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    // Far-future since — no entries match, but the route doesn't 500.
    const res = await fetch(
      `http://127.0.0.1:${port}/error-logs?since=${encodeURIComponent('9999-12-31T00:00:00Z')}`
    );
    const body = await res.json();
    server.close();
    expect(res.status).toBe(200);
    expect(body.success).toBe(true);
    expect(body.total).toBe(0);
    expect(body.logs).toEqual([]);
  });

  test('GET /error-logs combined filters compose correctly', async () => {
    const server = listen(buildRouter());
    const { port } = server.address();
    // level=WARN AND context=http: no entry matches (WARN is ssl-monitor).
    const res = await fetch(`http://127.0.0.1:${port}/error-logs?level=WARN&context=http`);
    const body = await res.json();
    server.close();
    expect(body.total).toBe(0);
    expect(body.logs).toEqual([]);
    expect(body.filters).toEqual({
      level: 'WARN', context: 'http', search: null,
      since: null, until: null,
    });
  });
});
