'use strict';

/**
 * tests/tz-compliance.js — проверка соответствия ТЗ через API
 * Запуск: node tests/tz-compliance.js
 * Требует: DEV_MODE=true, .env с БД
 */

require('dotenv').config();
const supertest = require('supertest');
const { pool } = require('../src/db');
const { deleteTestAudit, softDeleteByCIDR } = require('./helpers');
let pass = 0, fail = 0;
const errors = [];

function log(ok, name, detail = '') {
  if (ok) { console.log('  ✅', name); pass++; }
  else { const msg = '  ❌ ' + name + (detail ? ': ' + detail : ''); console.error(msg); errors.push(msg); fail++; }
}

function bearer(t) { return { Authorization: 'Bearer ' + t }; }
function cidr(ip) { return ip.includes('/') ? ip : ip + '/32'; }

(async () => {
  console.log('\n══════════════════════════════════════════');
  console.log('  ТЗ Compliance Check — v0.5.0');
  console.log('══════════════════════════════════════════\n');

  // ── Тестовые CIDR из «кривых» диапазонов ──
  // Фиксированные — cleanup всегда работает, даже между разными прогонами.
  // Проверка коллизий: если реальная запись существует, тест её не трогает.
  const C = {
    A:   '37.19.199.1/32',    B: '46.18.188.2/32',    C: '79.133.177.3/32',
    SUB: '91.210.166.4/32',   // для нормализации host bits
    M22: '176.120.0.0/22',    M24: '185.50.0.0/24',    M32: '212.8.155.7/32',
    ADM: '37.19.199.8/32',
  };
  // /30 для overlap — C.A=37.19.199.1, /30 покрывает 37.19.199.0-3 → включает C.A
  const overlapCIDR = C.A.replace(/\/32$/, '/30');
  // Нормализация: 91.210.166.5/24 → 91.210.166.0/24
  const subParts = C.SUB.replace(/\/32$/, '').split('.').map(Number);
  const subNormalized = subParts.slice(0,3).join('.') + '.0/24';

  const allTestCIDRs = [C.A, C.B, C.C, C.SUB, C.M22, C.M24, C.M32, C.ADM,
    subNormalized, overlapCIDR];

  console.log('  Тестовые CIDR:', C.A, C.B, C.C, '...');

  console.log('  Тестовые CIDR:', C.A, C.B, C.C, '...');

  // ── Pre-cleanup ──
  await softDeleteByCIDR(allTestCIDRs);
  console.log('  Pre-cleanup: тестовые CIDR очищены\n');

  const mod = require('../server');
  const app = await mod.start();
  const auth = await require('../src/auth').initAuth();
  const req = supertest(app);
  const { MOCK_USERS } = require('../src/config');

  const admin = MOCK_USERS.find(u => u.id === 'admin');
  const user = MOCK_USERS.find(u => u.id === 'alfa');
  const tokAdmin = auth.issueMockToken({ clientId: admin.clientId, companyId: admin.companyId, companyName: admin.companyName, email: admin.email });
  const tokUser = auth.issueMockToken({ clientId: user.clientId, companyId: user.companyId, companyName: user.companyName, email: user.email });

  // ══════════════════════════════════════════════════
  // 4.1. Просмотр списка записей
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.1: Просмотр списка ──');
  {
    const r = await req.get('/api/v1/entries').set(bearer(tokUser));
    log(r.status === 200, '4.1a: GET /entries → 200');
    const b = r.body || {};
    log(Array.isArray(b.entries), '4.1b: entries[] — массив');
    log(typeof b.limit === 'number', '4.1c: limit — число', `limit=${b.limit}`);
    log(typeof b.used === 'number', '4.1d: used — число (X из N)', `used=${b.used}`);
    log(b.used === b.entries.length, '4.1e: used === entries.length', `used=${b.used} len=${b.entries.length}`);
  }

  // 4.1f: Soft-deleted filter for admin
  {
    // Создаём и удаляем запись
    const cr = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'tz-del-test' });
    const eid = cr.body?.entry?.id;
    if (eid) {
      await req.delete('/api/v1/entries/' + eid).set(bearer(tokUser));
      // Admin: проверяем что GET /entries НЕ показывает удалённую запись
      const ar = await req.get('/api/v1/entries?company=' + (await (async () => {
        const c = await pool.query('SELECT id FROM companies WHERE client_id=$1', [user.clientId]);
        return c.rows[0]?.id;
      })())).set(bearer(tokAdmin));
      const found = (ar.body?.entries || []).find(e => e.id === eid);
      log(!found, '4.1f: soft-deleted скрыты от admin (по умолчанию)');
    }
    // Чистка
    await pool.query('UPDATE whitelist_entries SET deleted_at=NOW() WHERE value_cidr=$1 AND deleted_at IS NULL', [cidr(C.A)]);
  }

  // ══════════════════════════════════════════════════
  // 4.2. Создание записи
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.2: Создание записи ──');
  let entryId;
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'tz-create' });
    log(r.status === 201, '4.2a: POST /entries → 201', `status=${r.status}`);
    const b = r.body || {};
    log(!!b.entry?.id, '4.2b: ответ содержит entry.id');
    log(b.entry?.value_cidr === cidr(C.A), '4.2c: value_cidr сохранён верно');
    log(typeof b.wasNormalized === 'boolean', '4.2d: wasNormalized в ответе');
    entryId = b.entry?.id;
  }

  // 4.2e: Проверка лимита
  {
    // Узнаём текущий лимит и использовано
    const r = await req.get('/api/v1/entries').set(bearer(tokUser));
    const limit = r.body?.limit || 15;
    const used = r.body?.used || 1;
    log(used <= limit, `4.2e: used(${used}) ≤ limit(${limit})`);
  }

  // 4.2f: Дубликат → 409
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'dup' });
    log(r.status === 409, '4.2f: дубликат → 409', `status=${r.status}`);
  }

  // 4.2g: Запрещённый диапазон → 400
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: '192.168.1.1', comment: 'private' });
    log(r.status === 400, '4.2g: приватный IP → 400', `status=${r.status}`);
  }

  // 4.2h: Пустое значение → 400
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: '', comment: '' });
    log(r.status === 400, '4.2h: пустое значение → 400', `status=${r.status}`);
  }

  // 4.2i: Comment > 255 → 400
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.B, comment: 'x'.repeat(256) });
    log(r.status === 400, '4.2i: comment > 255 → 400', `status=${r.status}`);
  }

  // ══════════════════════════════════════════════════
  // 4.3. Редактирование записи
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.3: Редактирование ──');
  {
    const r = await req.patch('/api/v1/entries/' + entryId).set(bearer(tokUser)).send({ value: C.B, comment: 'updated' });
    log(r.status === 200, '4.3a: PATCH /entries/:id → 200', `status=${r.status}`);
    log(r.body?.entry?.value_cidr === cidr(C.B), '4.3b: value обновлён');
    log(r.body?.entry?.comment === 'updated', '4.3c: comment обновлён');
  }

  // 4.3d: Редактирование с пересечением → 409
  {
    // Создаём две записи: entryA с C.A, entryB с C.C
    const rA = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'keep-A' });
    const rB = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.C, comment: 'keep-C' });
    const idB = rB.body?.entry?.id;
    // Пытаемся обновить entryB на overlapCIDR (пересекается с C.A из entryA)
    const r = await req.patch('/api/v1/entries/' + idB).set(bearer(tokUser)).send({ value: overlapCIDR, comment: '' });
    log(r.status === 409, '4.3d: пересечение при edit → 409', `status=${r.status}`);
    await pool.query('UPDATE whitelist_entries SET deleted_at=NOW() WHERE value_cidr IN ($1,$2) AND deleted_at IS NULL', [cidr(C.A), cidr(C.C)]);
  }

  // 4.3e: Чужой пользователь не может редактировать
  {
    const r = await req.patch('/api/v1/entries/' + entryId).set(bearer(tokUser + 'WRONG')).send({ value: C.A, comment: '' });
    log(r.status === 401, '4.3e: невалидный токен → 401', `status=${r.status}`);
  }

  // ══════════════════════════════════════════════════
  // 4.4. Удаление (soft delete)
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.4: Soft delete ──');
  {
    const r = await req.delete('/api/v1/entries/' + entryId).set(bearer(tokUser));
    log([200, 204].includes(r.status), '4.4a: DELETE /entries/:id → 200/204', `status=${r.status}`);

    // Проверяем что запись не в списке
    const r2 = await req.get('/api/v1/entries').set(bearer(tokUser));
    const found = (r2.body?.entries || []).find(e => e.id === entryId);
    log(!found, '4.4b: удалённая запись не видна в GET /entries');

    // Проверяем что удаление освободило место в лимите
    const r3 = await req.get('/api/v1/entries').set(bearer(tokUser));
    log((r3.body?.used || 0) <= (r3.body?.limit || 15), '4.4c: used ≤ limit после удаления');
  }

  // ══════════════════════════════════════════════════
  // 4.5. Лимиты
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.5: Лимиты ──');
  {
    // 4.5a: Глобальный лимит по умолчанию = 15
    const r = await req.get('/api/v1/entries').set(bearer(tokUser));
    log(r.body?.limit > 0, '4.5a: limit > 0 (глобальный default)', `limit=${r.body?.limit}`);

    // 4.5b: Admin может установить per-company limit
    const comps = await req.get('/api/v1/companies').set(bearer(tokAdmin));
    const userCo = (comps.body?.companies || []).find(c => c.client_id === user.clientId);
    if (userCo) {
      // Установить лимит 3
      const r2 = await req.patch('/api/v1/companies/' + userCo.id + '/limit').set(bearer(tokAdmin)).send({ limit: 3 });
      log(r2.status === 200, '4.5b: admin setLimit → 200', `status=${r2.status}`);
      // Сбросить
      await req.patch('/api/v1/companies/' + userCo.id + '/limit').set(bearer(tokAdmin)).send({ limit: null });
    }

    // 4.5c: Обычный юзер не может менять лимит
    const r3 = await req.patch('/api/v1/companies/1/limit').set(bearer(tokUser)).send({ limit: 999 });
    log(r3.status === 403, '4.5c: user setLimit → 403', `status=${r3.status}`);
  }

  // ══════════════════════════════════════════════════
  // 4.6. Журнал аудита
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.6: Журнал аудита ──');
  {
    const r = await req.get('/api/v1/audit').set(bearer(tokAdmin));
    log(r.status === 200, '4.6a: GET /audit → 200 (admin)');
    log(Array.isArray(r.body?.rows), '4.6b: rows[] — массив');
    log(Array.isArray(r.body?.companies), '4.6c: companies[] — массив');

    // Проверяем что CREATE попал в аудит
    const createAudit = (r.body?.rows || []).find(a => a.action === 'CREATE');
    log(!!createAudit, '4.6d: CREATE записан в audit_log', createAudit ? `entry_id=${createAudit.entry_id}` : '');

    // User не может смотреть аудит
    const r2 = await req.get('/api/v1/audit').set(bearer(tokUser));
    log(r2.status === 403, '4.6e: user GET /audit → 403');
  }

  // ══════════════════════════════════════════════════
  // 4.7. Внешняя выдача (export)
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 4.7: Export ──');
  {
    // 4.7a: /export — публичный, без авторизации (ВРЕМЕННО)
    const r = await req.get('/export');
    log(r.status === 200, '4.7a: GET /export → 200 (без авторизации)', `status=${r.status}`);
    log((r.headers['content-type'] || '').includes('text/plain'), '4.7b: Content-Type text/plain');

    // 4.7c: /export содержит добавленный CIDR
    const cr = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: C.A, comment: 'export-test' });
    const r2 = await req.get('/export');
    log(r2.text.includes(cidr(C.A)), '4.7c: /export содержит добавленный CIDR');
  }

  // ══════════════════════════════════════════════════
  // 5. Валидация (Приложение А)
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 5 + Приложение А: Валидация ──');

  // 5a: Маски /22-/32
  {
    for (const [m, cidrVal] of [[22, C.M22], [24, C.M24], [32, C.M32]]) {
      const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: cidrVal, comment: `mask-${m}` });
      log(r.status === 201, `5.m${m}: /${m} → 201`, `status=${r.status}`);
    }
    for (const m of [21, 33]) {
      const badCIDR = C.M22.replace(/\/22$/, `/${m}`);
      const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: badCIDR, comment: `bad-${m}` });
      log(r.status === 400, `5.m${m}: /${m} → 400`, `status=${r.status}`);
    }
  }

  // 5b: Нормализация host bits
  {
    const subParts = C.SUB.split('/')[0].split('.');
    subParts[3] = '5'; // host=5, должен нормализоваться в .0
    const unnormalized = subParts.join('.') + '/24';
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: unnormalized, comment: 'normalize' });
    log(r.status === 201, '5.na: нормализация → 201');
    const expectedNet = subParts.slice(0,3).join('.') + '.0/24';
    log(r.body?.entry?.value_cidr === expectedNet, '5.nb: нормализован в ' + expectedNet, `got=${r.body?.entry?.value_cidr}`);
    log(r.body?.wasNormalized === true, '5.nc: wasNormalized=true');
  }

  // 5c: Все 14 запрещённых диапазонов
  {
    const blocked = [
      '10.0.0.1', '172.16.0.1', '192.168.0.1', '100.64.0.1',
      '127.0.0.1', '169.254.0.1', '192.0.0.1', '192.0.2.1',
      '198.51.100.1', '203.0.113.1', '198.18.0.1',
      '224.0.0.1', '240.0.0.1', '255.255.255.255',
    ];
    let allBlocked = true;
    for (const ip of blocked) {
      const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: ip, comment: 'blocked' });
      if (r.status !== 400) { allBlocked = false; break; }
    }
    log(allBlocked, '5.bl: все 14 запрещённых → 400');
  }

  // 5d: IPv6 → 400
  {
    const r = await req.post('/api/v1/entries').set(bearer(tokUser)).send({ value: '::1', comment: 'ipv6' });
    log(r.status === 400, '5.ip6: IPv6 → 400');
  }

  // 5e: Дубликаты внутри компании → 409
  // (уже проверено в 4.2f)

  // ══════════════════════════════════════════════════
  // 3. Роли и права
  // ══════════════════════════════════════════════════
  console.log('── ТЗ 3: Роли и права ──');

  // 3a: Admin видит все компании
  {
    const r = await req.get('/api/v1/companies').set(bearer(tokAdmin));
    const companies = r.body?.companies || [];
    log(companies.length >= 1, '3.aa: admin видит все компании', `count=${companies.length}`);
  }

  // 3b: Admin может CRUD в чужой компании
  {
    const userCo = (await pool.query('SELECT id FROM companies WHERE client_id=$1', [user.clientId])).rows[0];
    if (userCo) {
      const r = await req.post('/api/v1/entries?company=' + userCo.id).set(bearer(tokAdmin)).send({ value: C.ADM, comment: 'admin-crud' });
      log(r.status === 201, '3.ba: admin POST /entries?company= → 201', `status=${r.status}`);
      const eid = r.body?.entry?.id;
      if (eid) {
        await req.delete('/api/v1/entries/' + eid + '?company=' + userCo.id).set(bearer(tokAdmin));
      }
    }
  }

  // 3c: Обычный юзер не видит чужие компании
  {
    const r = await req.get('/api/v1/companies').set(bearer(tokUser));
    log(r.status === 403, '3.ca: user GET /companies → 403');
  }

  // ══════════════════════════════════════════════════
  // CLEANUP
  // ══════════════════════════════════════════════════
  console.log('\n── Cleanup ──');
  await softDeleteByCIDR(allTestCIDRs);
  await deleteTestAudit();
  await pool.end();

  console.log('\n══════════════════════════════════════════');
  console.log(`  ${pass} PASS, ${fail} FAIL`);
  if (errors.length) { console.log('\nПровалы:'); errors.forEach(e => console.log(' ', e)); }
  console.log('══════════════════════════════════════════');
  process.exit(fail > 0 ? 1 : 0);
})().catch(e => { console.error('FATAL:', e.message); pool.end(); process.exit(1); });
