'use strict';

/**
 * tests/integration.js — интеграционные тесты CRUD против реальной БД.
 *
 * Запускать: node tests/integration.js
 * Требует: .env с данными БД, DEV_MODE=true.
 *
 * Что тестируем:
 *   - Логин через /dev-login (получение сессии) от user и admin
 *   - GET / — страница загружается
 *   - POST /add — создание записи
 *   - POST /edit/:id — редактирование
 *   - POST /delete/:id — удаление
 *   - Изоляция: user не видит чужие записи через company_id
 *   - Лимит: создание сверх лимита → ошибка
 *   - Дубликат CIDR → ошибка
 *   - Невалидный CIDR → ошибка
 *   - Admin: может работать с любой компанией
 */

require('dotenv').config();

const supertest   = require('supertest');
const { pool }    = require('../src/db');

let pass = 0, fail = 0;
const errors = [];

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

// ── Хелперы ──────────────────────────────────────────────────────────────────

/**
 * Создаёт агент (сессию) и логинится под нужным пользователем.
 * Возвращает agent с уже установленными cookies.
 */
async function loginAs(app, userId) {
  const agent = supertest.agent(app);

  // GET /dev-login → берём CSRF-токен из скрытого поля формы
  const getRes = await agent.get('/dev-login');
  if (getRes.status !== 200) throw new Error(`GET /dev-login: ${getRes.status}`);

  const csrfMatch = getRes.text.match(/name="_csrf"\s+value="([^"]+)"/);
  if (!csrfMatch) throw new Error('CSRF-токен не найден в форме /dev-login');
  const csrfToken = csrfMatch[1];

  // POST /dev-login — логинимся
  const postRes = await agent
    .post('/dev-login')
    .type('form')
    .send({ preset: userId, _csrf: csrfToken });

  // Ожидаем редирект на /
  if (postRes.status !== 302 && postRes.status !== 200) {
    throw new Error(`POST /dev-login: ${postRes.status} ${postRes.text.slice(0, 200)}`);
  }

  return agent;
}

/**
 * Получает CSRF-токен со страницы / (нужен для POST /add, /edit, /delete).
 */
async function getCsrfFromIndex(agent) {
  const res = await agent.get('/');
  if (res.status !== 200) throw new Error(`GET /: ${res.status}`);
  const m = res.text.match(/name="_csrf"\s+value="([^"]+)"/);
  if (!m) throw new Error('CSRF-токен не найден на главной странице');
  return m[1];
}

/**
 * Извлекает query-параметр из Location-заголовка редиректа.
 */
function getRedirectParam(res, param) {
  const loc = res.headers.location || '';
  try {
    // Location может быть относительным — добавим базу
    const url = new URL(loc, 'http://localhost');
    return url.searchParams.get(param);
  } catch {
    return null;
  }
}

// ── Тестовые данные ─────────────────────────────────────────────────────────
// Используем фиксированные публичные IP из диапазона TEST-NET, не в BLOCKED_RANGES.
// Cleanup удаляет их по company после тестов.
const TEST_CIDR_1 = '8.8.4.4';      // добавим, проверим дубликат и удалим
const TEST_CIDR_2 = '9.9.9.5/24';   // нормализуется в 9.9.9.0/24
const TEST_CIDR_OVERLAP = '9.9.9.100'; // пересекается с 9.9.9.0/24
const TEST_CIDR_ADMIN = '1.1.1.100'; // admin добавит в компанию user

const TEST_CLIENT_IDS = ['TEST-INTEG-001', 'TEST-INTEG-002'];

// ── Чистка тестовых данных ────────────────────────────────────────────────────
async function cleanup() {
  // Удаляем конкретные тестовые CIDR из компаний WZ01325 и WZ01112
  const testCidrs = ['8.8.4.4/32', '8.8.4.5/32', '9.9.9.0/24', '9.9.9.100/32', '1.1.1.100/32'];
  for (const cidr of testCidrs) {
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = $1 AND deleted_at IS NULL`, [cidr]
    );
  }
  // Удаляем тестовые компании и все их записи (каскадно через транзакцию)
  for (const cid of TEST_CLIENT_IDS) {
    const comp = await pool.query('SELECT id FROM companies WHERE client_id = $1', [cid]);
    if (comp.rows.length === 0) continue;
    const compId = comp.rows[0].id;
    await pool.query('DELETE FROM audit_log WHERE company_id = $1', [compId]);
    await pool.query('DELETE FROM whitelist_entries WHERE company_id = $1', [compId]);
    await pool.query('DELETE FROM companies WHERE id = $1', [compId]);
  }
}

// ── Основной тест-раннер ──────────────────────────────────────────────────────

(async () => {
  console.log('\n══════════════════════════════════════════════');
  console.log('  Интеграционные тесты (реальная БД)');
  console.log('══════════════════════════════════════════════\n');

  // Запускаем сервер
  let server, app;
  try {
    // server.js экспортирует app после start()
    const mod = require('../server');
    app = await mod.start();
  } catch (e) {
    console.error('FATAL: не удалось запустить сервер:', e.message);
    await pool.end();
    process.exit(1);
  }

  await cleanup();
  console.log('── Cleanup done ──\n');

  // ─────────────────────────────────────────────────────────────────────────────
  // 1. Логин
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('── 1. Логин ──');

  let userAgent, adminAgent, user2Agent;
  try {
    userAgent  = await loginAs(app, 'test');
    log(true, 'login as user (test)');
  } catch (e) { log(false, 'login as user (test)', e.message); }

  try {
    adminAgent = await loginAs(app, 'admin');
    log(true, 'login as admin');
  } catch (e) { log(false, 'login as admin', e.message); }

  try {
    user2Agent = await loginAs(app, 'client2');
    log(true, 'login as user2 (client2)');
  } catch (e) { log(false, 'login as user2 (client2)', e.message); }

  if (!userAgent || !adminAgent) {
    console.error('\nFATAL: не удалось залогиниться, прерываем тесты');
    await cleanup();
    await pool.end();
    process.exit(1);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 2. GET / — страница загружается
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 2. GET / ──');

  {
    const res = await userAgent.get('/');
    log(res.status === 200, 'user: GET / → 200');
    log(res.text.includes('Доверенные адреса'), 'user: страница содержит "Доверенные адреса"');
    // company встречается в скрытых полях форм — это нормально
    log(!res.text.includes('<select name="company"') || res.text.includes('isAdmin') === false, 'user: нет select переключателя компаний');
  }
  {
    const res = await adminAgent.get('/');
    log(res.status === 200, 'admin: GET / → 200');
    log(res.text.includes('Доверенные адреса'), 'admin: страница загружена');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 3. POST /add — создание записей от user
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 3. POST /add (user) ──');

  let entryId; // сохраним для edit/delete

  {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: TEST_CIDR_1, comment: 'тест 1', _csrf: csrf });
    log(res.status === 302, 'user: POST /add 10.10.10.1 → redirect');
    const msg = getRedirectParam(res, 'message');
    const err = getRedirectParam(res, 'error');
    log(!err && (msg === 'Добавлено' || msg?.includes('нормализован')),
      'user: /add → сообщение "Добавлено"', `msg=${msg} err=${err}`);
  }

  {
    // Нормализация: 9.9.9.5/24 → 9.9.9.0/24
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: TEST_CIDR_2, comment: 'нормализация', _csrf: csrf });
    log(res.status === 302, 'user: POST /add 10.20.30.5/24 → redirect');
    const msg = getRedirectParam(res, 'message');
    log(msg?.includes('нормализован'), 'user: /add → адрес нормализован', `msg=${msg}`);
  }

  // Сохраняем id добавленной записи для дальнейших тестов
  // Читаем из БД напрямую — ищем TEST_CIDR_1 (8.8.4.4/32)
  {
    const r = await pool.query(
      `SELECT id FROM whitelist_entries WHERE value_cidr = $1 AND deleted_at IS NULL`,
      [TEST_CIDR_1 + '/32']
    );
    entryId = r.rows[0]?.id || null;
    log(!!entryId, 'user: нашли entryId в странице', `entryId=${entryId}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 4. POST /add — дубликат → ошибка
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 4. Дубликат CIDR ──');

  {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: TEST_CIDR_1, comment: 'дубликат', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && err?.includes('уже существует'),
      'user: дубликат → ошибка "уже существует"', `err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 5. POST /add — невалидный CIDR → ошибка
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 5. Невалидный CIDR ──');

  {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: 'не-адрес', comment: '', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err, 'user: невалидный CIDR → ошибка', `err=${err}`);
  }

  {
    // Октеты > 255
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: '999.0.0.1', comment: '', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err, 'user: октет > 255 → ошибка', `err=${err}`);
  }

  {
    // Маска > 32
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: '10.0.0.1/33', comment: '', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err, 'user: маска /33 → ошибка', `err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 6. POST /edit/:id — редактирование
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 6. POST /edit (user) ──');

  if (entryId) {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post(`/edit/${entryId}`).type('form')
      .send({ value: '8.8.4.5', comment: 'обновлено', _csrf: csrf });
    log(res.status === 302, `user: POST /edit/${entryId} → redirect`);
    const msg = getRedirectParam(res, 'message');
    const err = getRedirectParam(res, 'error');
    log(!err && (msg === 'Сохранено' || msg?.includes('нормализован')),
      'user: /edit → "Сохранено"', `msg=${msg} err=${err}`);
  } else {
    log(false, 'user: /edit — нет entryId, тест пропущен');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 7. Изоляция: user2 не может редактировать чужую запись
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 7. Изоляция между пользователями ──');

  if (entryId && user2Agent) {
    const csrf = await getCsrfFromIndex(user2Agent);
    const res  = await user2Agent.post(`/edit/${entryId}`).type('form')
      .send({ value: '1.2.3.4', comment: 'чужая', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err,
      'user2: edit чужой записи → ошибка (изоляция)', `err=${err}`);
  } else {
    log(false, 'изоляция: нет entryId или user2Agent');
  }

  if (entryId && user2Agent) {
    const csrf = await getCsrfFromIndex(user2Agent);
    const res  = await user2Agent.post(`/delete/${entryId}`).type('form')
      .send({ _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err,
      'user2: delete чужой записи → ошибка (изоляция)', `err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 8. Пересечение CIDR → ошибка
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 8. Пересечение CIDR ──');

  {
    // 9.9.9.100 пересекается с 9.9.9.0/24 (уже добавлен)
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post('/add').type('form')
      .send({ value: TEST_CIDR_OVERLAP, comment: 'пересечение', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && err?.includes('Пересечение'),
      'user: CIDR пересекается с существующим → ошибка', `err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 9. Admin — работа с компанией пользователя
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 9. Admin CRUD по компании user ──');

  // Получаем id компании user из БД
  const userComp = await pool.query(
    "SELECT id FROM companies WHERE client_id = 'WZ01325'"
  );
  const userCompId = userComp.rows[0]?.id;
  log(!!userCompId, 'admin: нашли компанию user в БД', `id=${userCompId}`);

  if (userCompId) {
    // Admin добавляет запись в компанию user
    const csrf = await getCsrfFromIndex(adminAgent);
    const res  = await adminAgent.post('/add').type('form')
      .send({ value: TEST_CIDR_ADMIN, comment: 'от admin', company_id: userCompId, _csrf: csrf });
    log(res.status === 302, 'admin: POST /add в чужую компанию → redirect');
    const msg = getRedirectParam(res, 'message');
    const err = getRedirectParam(res, 'error');
    log(!err && (msg === 'Добавлено' || msg?.includes('нормализован')),
      'admin: /add в компанию user → Добавлено', `msg=${msg} err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 10. POST /delete/:id — удаление (user своей записи)
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 10. POST /delete (user) ──');

  if (entryId) {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post(`/delete/${entryId}`).type('form')
      .send({ _csrf: csrf });
    log(res.status === 302, `user: POST /delete/${entryId} → redirect`);
    const msg = getRedirectParam(res, 'message');
    const err = getRedirectParam(res, 'error');
    log(!err && msg === 'Запись удалена', 'user: /delete → "Запись удалена"', `msg=${msg} err=${err}`);

    // Проверяем что мягко удалена (deleted_at выставлен)
    const row = await pool.query(
      'SELECT deleted_at FROM whitelist_entries WHERE id = $1', [entryId]
    );
    log(!!row.rows[0]?.deleted_at, 'user: deleted_at выставлен в БД (soft delete)');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 11. Повторное удаление удалённой записи → ошибка
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 11. Повторное удаление ──');

  if (entryId) {
    const csrf = await getCsrfFromIndex(userAgent);
    const res  = await userAgent.post(`/delete/${entryId}`).type('form')
      .send({ _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302 && !!err,
      'user: повторное /delete удалённой записи → ошибка', `err=${err}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 12. Неавторизованный доступ → редирект на /login
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 12. Неавторизованный доступ ──');

  {
    const res = await supertest(app).get('/');
    log(res.status === 302 && res.headers.location?.includes('login'),
      'анонимный: GET / → redirect /login', `loc=${res.headers.location}`);
  }

  {
    const res = await supertest(app).post('/add').type('form').send({ value: '1.2.3.4' });
    // CSRF middleware отдаёт 401 до auth.middleware — анонимный не проходит
    log([302, 401, 403].includes(res.status),
      'анонимный: POST /add → не 200 (302/401/403)', `status=${res.status}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 13. /export — изоляция
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 13. GET /export ──');

  {
    const res = await userAgent.get('/export');
    log(res.status === 200, 'user: GET /export → 200');
    // CIDR другой компании не должен попасть
    const text = res.text;
    // 172.16.0.1 добавил admin в компанию user — должен быть в экспорте
    // но не должно быть CIDRов от WZ02001
    log(!text.includes('WZ02001'), 'user: /export не содержит данных другой компании');
  }

  {
    const res = await adminAgent.get('/export');
    log(res.status === 200, 'admin: GET /export → 200 (все компании)');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 14. Лимит — добавить сверх лимита → ошибка
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 14. Лимит ──');

  {
    // client_id пользователя 'test' — WZ01325 (из MOCK_USERS)
    const { MOCK_USERS } = require('../src/config');
    const testUser = MOCK_USERS.find(u => u.id === 'test');
    const userClientId = testUser?.clientId;

    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`,
      [userClientId]
    );
    const userCompanyId = uRes.rows[0]?.id;

    if (userCompanyId) {
      // Считаем активные записи компании
      const cntRes = await pool.query(
        `SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
        [userCompanyId]
      );
      const currentCount = cntRes.rows[0].c;

      // Ставим лимит = текущее количество записей → следующее добавление упадёт
      const csrf = await getCsrfFromIndex(adminAgent);
      const limitRes = await adminAgent.post(`/admin/limit/${userCompanyId}`).type('form')
        .send({ limit: String(currentCount), _csrf: csrf });
      log(limitRes.status === 302, 'admin: выставил лимит=currentCount для user', `status=${limitRes.status} count=${currentCount}`);

      // Добавляем запись — должна упасть с "Лимит исчерпан"
      const csrf3 = await getCsrfFromIndex(userAgent);
      const r2 = await userAgent.post('/add').type('form')
        .send({ value: '8.8.4.100', comment: 'лимит тест', _csrf: csrf3 });
      const err2 = getRedirectParam(r2, 'error');
      log(err2?.includes('Лимит'), 'user: лимит исчерпан → ошибка', `err=${err2}`);

      // Возвращаем лимит обратно (пустая строка = дефолт)
      const csrf4 = await getCsrfFromIndex(adminAgent);
      await adminAgent.post(`/admin/limit/${userCompanyId}`).type('form')
        .send({ limit: '', _csrf: csrf4 });

      // Чистим тестовые записи лимит-теста
      await pool.query(
        `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
         WHERE value_cidr IN ('8.8.8.8/32', '8.8.4.100/32') AND deleted_at IS NULL`
      );
    } else {
      log(false, 'admin: не нашли компанию user для теста лимита');
      log(false, 'user: лимит исчерпан → ошибка');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 15. Audit log — операции пишутся в БД
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 15. Audit log ──');

  {
    const { MOCK_USERS } = require('../src/config');
    const testUser = MOCK_USERS.find(u => u.id === 'test');
    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`,
      [testUser?.clientId]
    );
    const userCompanyId = uRes.rows[0]?.id;
    if (userCompanyId) {
      const r = await pool.query(
        `SELECT action FROM audit_log WHERE company_id = $1 ORDER BY created_at DESC LIMIT 20`,
        [userCompanyId]
      );
      const actions = r.rows.map(row => row.action);
      log(actions.some(a => /add|create/i.test(a)),
        'audit: операция add записана в audit_log', `actions=${JSON.stringify(actions.slice(0,5))}`);
      log(actions.some(a => /delete/i.test(a)),
        'audit: операция delete записана в audit_log', `actions=${JSON.stringify(actions.slice(0,5))}`);
    } else {
      log(false, 'audit: не нашли компанию user');
      log(false, 'audit: не нашли компанию user (delete)');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 16. Безопасность — XSS и спецсимволы в comment
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 16. Безопасность: XSS в comment ──');

  {
    const xssPayload = '<script>alert(1)</script>';
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '5.5.5.5', comment: xssPayload, _csrf: csrf });
    // Запись могла добавиться или нет (зависит от валидации comment)
    const err = getRedirectParam(res, 'error');

    // Если добавилась — проверяем что скрипт не выполняется в HTML
    const page = await userAgent.get('/');
    const rawScript = page.text.includes('<script>alert(1)</script>');
    log(!rawScript, 'security: XSS в comment — скрипт не рендерится в HTML',
      rawScript ? 'RAW SCRIPT FOUND — XSS!' : `escaped или отклонено (err=${err})`);

    // Чистим
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '5.5.5.5/32' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 17. Rate-limiter — перенесён в конец, после всех тестов использующих userAgent
  // ─────────────────────────────────────────────────────────────────────────────

  // ─────────────────────────────────────────────────────────────────────────────
  // 18. IDOR — user пытается добавить запись в чужую компанию через company_id
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 18. IDOR: подделка company_id ──');

  {
    // user2 (WZ02001) пытается добавить запись в компанию user (WZ01325)
    const { MOCK_USERS } = require('../src/config');
    const userClient  = MOCK_USERS.find(u => u.id === 'test')?.clientId;
    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [userClient]
    );
    const userCompId = uRes.rows[0]?.id;

    if (userCompId && user2Agent) {
      const countBefore = (await pool.query(
        `SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
        [userCompId]
      )).rows[0].c;

      const csrf = await getCsrfFromIndex(user2Agent);
      // user2 подделывает company_id = чужой компании
      const res = await user2Agent.post('/add').type('form')
        .send({ value: '5.6.7.8', comment: 'IDOR test', company_id: userCompId, _csrf: csrf });

      const countAfter = (await pool.query(
        `SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
        [userCompId]
      )).rows[0].c;

      log(countBefore === countAfter,
        'IDOR: user2 не смог добавить в компанию user через company_id',
        `before=${countBefore} after=${countAfter}`);

      // Чистим на случай если баг сработал
      await pool.query(
        `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
         WHERE value_cidr = '5.6.7.8/32' AND deleted_at IS NULL`
      );
    } else {
      log(false, 'IDOR: нет userCompId или user2Agent');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 19. /admin страница — user не может зайти
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 19. /admin — только для admin ──');

  {
    const res = await userAgent.get('/admin');
    log(res.status === 302 || res.status === 403,
      'user: GET /admin → redirect или 403', `status=${res.status}`);
  }

  {
    const res = await adminAgent.get('/admin');
    log(res.status === 200, 'admin: GET /admin → 200', `status=${res.status}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 20. /audit — только для admin, user видит только свои
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 20. /audit ──');

  {
    const res = await userAgent.get('/audit');
    // /audit только для admin — user получает 403
    log(res.status === 403 || res.status === 302,
      'user: GET /audit → 403 (нет доступа)', `status=${res.status}`);
  }

  {
    const res = await adminAgent.get('/audit');
    log(res.status === 200, 'admin: GET /audit → 200', `status=${res.status}`);
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 21. IPv6 адреса — валидация
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 21. IPv6 ──');

  {
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '2001:db8::1', comment: 'ipv6 test', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    const msg = getRedirectParam(res, 'message');
    // Либо принимает IPv6, либо отклоняет как несуппортируемый формат
    log(res.status === 302, 'user: IPv6 → redirect (не падает 500)', `status=${res.status} msg=${msg} err=${err}`);

    // Чистим если добавился
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr LIKE '%2001:db8%' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 22. Граничные CIDR — /0 и /32
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 22. Граничные CIDR ──');

  {
    // /0 — весь интернет, должно быть отклонено или принято
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '0.0.0.0/0', comment: 'весь интернет', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(res.status === 302, 'user: 0.0.0.0/0 → redirect (не 500)', `err=${err}`);
    // Чистим
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '0.0.0.0/0' AND deleted_at IS NULL`
    );
  }

  {
    // /32 с host-битами — нормализация не нужна, должен добавиться как есть
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '5.5.5.5/32', comment: 'slash32', _csrf: csrf });
    const msg = getRedirectParam(res, 'message');
    const err = getRedirectParam(res, 'error');
    log(!err && res.status === 302, 'user: 5.5.5.5/32 → добавлено', `msg=${msg} err=${err}`);
    // Чистим
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '5.5.5.5/32' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 26. Пустой comment — должен сохранять запись без ошибки
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 26. Пустой comment ──');

  {
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '7.7.7.7', comment: '', _csrf: csrf });
    const err = getRedirectParam(res, 'error');
    log(!err && res.status === 302, 'user: добавил запись без comment', `err=${err}`);
    // Чистим
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '7.7.7.7/32' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 27. Export содержит CIDR текущей компании
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 27. Export содержит CIDR ──');

  {
    // Добавим уникальную запись и проверим что она появляется в export
    const csrf = await getCsrfFromIndex(userAgent);
    await userAgent.post('/add').type('form')
      .send({ value: '6.6.6.6', comment: 'export test', _csrf: csrf });

    const res = await userAgent.get('/export');
    log(res.status === 200, 'user: /export → 200');
    const hasCidr = res.text.includes('6.6.6.6');
    log(hasCidr, 'user: добавленный CIDR виден в /export',
      hasCidr ? 'found' : `text=${res.text.slice(0, 100)}`);

    // Чистим
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '6.6.6.6/32' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 28. Удалённая запись не видна в export
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 28. Удалённая запись не в export ──');

  {
    // Добавляем, потом удаляем, проверяем что нет в export
    const csrf = await getCsrfFromIndex(userAgent);
    const addRes = await userAgent.post('/add').type('form')
      .send({ value: '4.4.4.4', comment: 'будет удалена', _csrf: csrf });

    // Ищем id в БД
    const r = await pool.query(
      `SELECT id FROM whitelist_entries WHERE value_cidr = '4.4.4.4/32' AND deleted_at IS NULL`
    );
    const delId = r.rows[0]?.id;
    if (delId) {
      const csrf2 = await getCsrfFromIndex(userAgent);
      await userAgent.post(`/delete/${delId}`).type('form').send({ _csrf: csrf2 });
    }

    const exportRes = await userAgent.get('/export');
    log(!exportRes.text.includes('4.4.4.4'),
      'user: удалённый CIDR не виден в /export');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 29. Очень длинный comment — не должен падать с 500
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 29. Длинный comment ──');

  {
    const longComment = 'A'.repeat(10000);
    const csrf = await getCsrfFromIndex(userAgent);
    const res = await userAgent.post('/add').type('form')
      .send({ value: '3.3.3.3', comment: longComment, _csrf: csrf });
    log(res.status === 302, 'user: очень длинный comment → redirect (не 500)', `status=${res.status}`);
    const err = getRedirectParam(res, 'error');
    // Либо добавилось, либо ошибка валидации — но не 500
    log(res.status !== 500, 'user: длинный comment не вызывает 500', `err=${err}`);
    await pool.query(
      `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
       WHERE value_cidr = '3.3.3.3/32' AND deleted_at IS NULL`
    );
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 30. Admin limit = 0 — компания сразу не может добавить ничего
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 30. Лимит = 0 ──');

  {
    const { MOCK_USERS } = require('../src/config');
    const testUser = MOCK_USERS.find(u => u.id === 'test');
    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [testUser?.clientId]
    );
    const userCompId = uRes.rows[0]?.id;

    if (userCompId) {
      const csrf = await getCsrfFromIndex(adminAgent);
      const limitRes = await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
        .send({ limit: '0', _csrf: csrf });
      log(limitRes.status === 302, 'admin: выставил лимит=0', `status=${limitRes.status}`);

      const csrf2 = await getCsrfFromIndex(userAgent);
      const addRes = await userAgent.post('/add').type('form')
        .send({ value: '2.2.2.2', comment: 'лимит 0', _csrf: csrf2 });
      const err = getRedirectParam(addRes, 'error');
      log(err?.includes('Лимит'), 'user: лимит=0 → любое добавление = ошибка', `err=${err}`);

      // Сброс
      const csrf3 = await getCsrfFromIndex(adminAgent);
      await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
        .send({ limit: '', _csrf: csrf3 });
    } else {
      log(false, 'admin: нет компании для теста лимита=0');
      log(false, 'user: лимит=0 test пропущен');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 31. Admin limit с невалидным значением — не должен сломать лимит
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 31. Невалидный лимит ──');

  {
    const { MOCK_USERS } = require('../src/config');
    const testUser = MOCK_USERS.find(u => u.id === 'test');
    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [testUser?.clientId]
    );
    const userCompId = uRes.rows[0]?.id;

    if (userCompId) {
      const csrf = await getCsrfFromIndex(adminAgent);
      const r = await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
        .send({ limit: '-5', _csrf: csrf });
      // Должен либо отклонить (ошибка), либо принять как 0
      log(r.status === 302 || r.status === 400,
        'admin: лимит=-5 → redirect или 400', `status=${r.status}`);

      // Проверяем что limit в БД не стал отрицательным
      const limRes = await pool.query(
        `SELECT custom_limit FROM companies WHERE id = $1`, [userCompId]
      );
      const savedLimit = limRes.rows[0]?.custom_limit;
      log(savedLimit === null || savedLimit >= 0,
        'admin: custom_limit в БД не отрицательный', `saved=${savedLimit}`);

      // Чистим если записался -5
      if (savedLimit !== null && savedLimit < 0) {
        await pool.query(`UPDATE companies SET custom_limit = NULL WHERE id = $1`, [userCompId]);
      }
    } else {
      log(false, 'admin: нет компании для теста невалидного лимита');
      log(false, 'admin: пропущено');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 24. Race condition — одновременные запросы на /add при лимите
  // (используем свежий агент, не отравленный rate-limit)
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 24. Race condition: лимит ──');

  {
    const { MOCK_USERS } = require('../src/config');
    const testUser = MOCK_USERS.find(u => u.id === 'test');
    const uRes = await pool.query(
      `SELECT id FROM companies WHERE client_id = $1 LIMIT 1`, [testUser?.clientId]
    );
    const userCompId = uRes.rows[0]?.id;

    if (userCompId) {
      // Свежий агент — не отравлен rate-limiter
      const raceAgent = await loginAs(app, 'test');

      const cntRes = await pool.query(
        `SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL`,
        [userCompId]
      );
      const count = cntRes.rows[0].c;
      const raceLimit = count + 1;

      const csrf = await getCsrfFromIndex(adminAgent);
      await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
        .send({ limit: String(raceLimit), _csrf: csrf });

      // 5 параллельных запросов — только 1 должен пройти
      const raceCidrs = ['11.11.11.1', '11.11.11.2', '11.11.11.3', '11.11.11.4', '11.11.11.5'];
      const results = await Promise.all(raceCidrs.map(async (cidr) => {
        const c = await getCsrfFromIndex(raceAgent);
        const r = await raceAgent.post('/add').type('form')
          .send({ value: cidr, comment: 'race', _csrf: c });
        const st = r.status;
        return { cidr, status: st, err: getRedirectParam(r, 'error'), msg: getRedirectParam(r, 'message') };
      }));

      // Считаем фактически добавленные в БД
      const dbRes = await pool.query(
        `SELECT COUNT(*)::int AS c FROM whitelist_entries
         WHERE company_id = $1 AND value_cidr IN ('11.11.11.1/32','11.11.11.2/32','11.11.11.3/32','11.11.11.4/32','11.11.11.5/32')
           AND deleted_at IS NULL`, [userCompId]
      );
      const addedInDb = dbRes.rows[0].c;
      log(addedInDb <= 1, 'race: в БД не более 1 записи (FOR UPDATE работает)',
        `added=${addedInDb} results=${JSON.stringify(results.map(r => ({cidr: r.cidr, err: r.err?.slice(0,20)})))}`);

      // Сброс лимита
      const csrf2 = await getCsrfFromIndex(adminAgent);
      await adminAgent.post(`/admin/limit/${userCompId}`).type('form')
        .send({ limit: '', _csrf: csrf2 });

      // Чистим
      await pool.query(
        `UPDATE whitelist_entries SET deleted_at = NOW(), deleted_by = 'test-cleanup'
         WHERE value_cidr IN ('11.11.11.1/32','11.11.11.2/32','11.11.11.3/32','11.11.11.4/32','11.11.11.5/32')
           AND deleted_at IS NULL`
      );
    } else {
      log(false, 'race: нет компании user');
    }
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // 25. Rate-limiter — превысить mutationLimiter (30 POST/min)
  // Этот тест ПОСЛЕДНИЙ — исчерпывает rate-limit для userAgent
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── 25. Rate-limiter ──');

  {
    // Свежий агент — чтобы не зависеть от состояния userAgent
    const rateAgent = await loginAs(app, 'test');
    let got429 = false;
    for (let i = 0; i < 35; i++) {
      const csrf = await getCsrfFromIndex(rateAgent);
      const res = await rateAgent.post('/add').type('form')
        .send({ value: 'not-an-ip', comment: `rate ${i}`, _csrf: csrf });
      if (res.status === 429) { got429 = true; break; }
    }
    log(got429, 'rate-limiter: POST /add → 429 после 30 запросов');
  }

  // ─────────────────────────────────────────────────────────────────────────────
  // Cleanup
  // ─────────────────────────────────────────────────────────────────────────────
  console.log('\n── Cleanup ──');
  await cleanup();
  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('\nFATAL:', e.message);
  pool.end();
  process.exit(1);
});
