'use strict';

/**
 * tests/run-tests.js — smoke/unit тесты без внешних зависимостей.
 * Запускать: node tests/run-tests.js
 * БД не нужна.
 */

let pass = 0, fail = 0;

function test(name, actual, expected) {
  if (actual === expected) {
    console.log('  PASS ' + name);
    pass++;
  } else {
    console.error(' FAIL ' + name + ': got ' + JSON.stringify(actual) + ', want ' + JSON.stringify(expected));
    fail++;
  }
}

function testThrows(name, fn) {
  try {
    fn();
    console.error(' FAIL ' + name + ': expected throw, got no error');
    fail++;
  } catch (e) {
    console.log('  PASS ' + name + ' (throws: ' + e.message + ')');
    pass++;
  }
}

function section(title) {
  console.log('\n── ' + title + ' ──');
}

// ────────────────────────────────────────────────────────────────
// 1. Модули загружаются без ошибок
// ────────────────────────────────────────────────────────────────
section('Загрузка модулей');

const modules = [
  'src/config.js',
  'src/auth.js',
  'src/queries.js',
  'src/validators.js',
  'src/middleware/rateLimit.js',
  'src/middleware/csrf.js',
  'src/api/index.js',
  'src/api/routes/entries.js',
  'src/api/routes/admin.js',
  'ui/index.js',
  'ui/routes/auth.js',
  'ui/routes/entries.js',
  'ui/routes/admin.js',
  'ui/routes/export.js',
  'server.js',
];

for (const m of modules) {
  try {
    require('../' + m);
    test('require ' + m, true, true);
  } catch (e) {
    test('require ' + m, false, true);
  }
}

// ────────────────────────────────────────────────────────────────
// 2. src/config.js
// ────────────────────────────────────────────────────────────────
section('config.js');

const { MOCK_USERS, backUrl } = require('../src/config');

test('MOCK_USERS.length === 4', MOCK_USERS.length, 4);
test('admin clientId', MOCK_USERS.some(u => u.clientId === 'WZ01112'), true);
test('all have companyId', MOCK_USERS.every(u => !!u.companyId), true);
test('admin backUrl has company', backUrl(true, 5, {}).includes('company=5'), true);
test('user backUrl has no company', backUrl(false, 5, {}).includes('company='), false);
test('extra propagated', backUrl(true, 5, { error: 'foo' }).includes('error=foo'), true);
test('empty → /', backUrl(false, null, {}), '/');

// ────────────────────────────────────────────────────────────────
// 3. src/validators.js
// ────────────────────────────────────────────────────────────────
section('validators.js');

const { aggregateCIDRs, validate, overlaps } = require('../src/validators');

// aggregateCIDRs
test('single /32', aggregateCIDRs(['203.0.114.1/32']).join(), '203.0.114.1/32');
test('adjacent /32 → /31', aggregateCIDRs(['8.8.8.0/32', '8.8.8.1/32']).join(), '8.8.8.0/31');
test('subnet passthrough /24', aggregateCIDRs(['1.2.3.0/24']).join(), '1.2.3.0/24');
test('empty → []', aggregateCIDRs([]).length, 0);

// validate — корректные входные данные
try {
  const r = validate('8.8.8.8/32');
  test('valid /32 cidr', r.cidr, '8.8.8.8/32');
  test('valid /32 wasNormalized', r.wasNormalized, false);
} catch (e) { test('valid /32', false, true); }

try {
  const r = validate('8.8.8.8');  // без маски → /32
  test('no-mask → /32', r.cidr, '8.8.8.8/32');
} catch (e) { test('no-mask → /32', false, true); }

try {
  const r = validate('1.2.3.7/24');  // нормализация host-битов
  test('normalize host bits', r.cidr, '1.2.3.0/24');
  test('wasNormalized=true', r.wasNormalized, true);
} catch (e) { test('normalize host bits', false, true); }

try {
  const r = validate('4.4.0.0/22');  // минимальная допустимая маска
  test('min mask /22', r.cidr, '4.4.0.0/22');
} catch (e) { test('min mask /22', false, true); }

// validate — некорректные (должны бросать)
testThrows('empty throws', () => validate(''));
testThrows('private 10.x throws', () => validate('10.1.2.3/32'));
testThrows('private 192.168 throws', () => validate('192.168.1.0/24'));
testThrows('mask /8 throws', () => validate('1.2.3.4/8'));
testThrows('mask /21 throws', () => validate('1.2.3.0/21'));
testThrows('bad IP throws', () => validate('999.0.0.1/32'));
testThrows('IPv6 throws', () => validate('::1'));

// overlaps
test('overlap subnet in block', overlaps('10.0.1.0/24', '10.0.0.0/8'), true);
test('overlap exact match', overlaps('192.168.0.0/24', '192.168.0.0/24'), true);
test('no overlap public', overlaps('8.8.8.0/24', '9.9.9.0/24'), false);

// ────────────────────────────────────────────────────────────────
// 4. src/auth.js — session middleware (async, без JWT, без БД)
// ────────────────────────────────────────────────────────────────

process.env.DEV_MODE = 'true';
process.env.ADMIN_CLIENT_ID = 'WZ01112';

const { initAuth, requireAdmin, safeReturn } = require('../src/auth');
const csrfMiddleware             = require('../src/middleware/csrf');

(async () => {

section('auth.js (session middleware)');

try {
  const auth = await initAuth();
  test('middleware is function',           typeof auth.middleware,      'function');
  test('jwksHandler present (mock mode)',  typeof auth.jwksHandler,     'function');
  test('devLoginEnabled when DEV_MODE',    auth.devLoginEnabled,        true);
  test('isOidc false (no KC creds)',       auth.isOidc,                 false);

  // ── Пользователь из session ──
  const mockUser = { clientId: 'WZ01325', companyId: 'test-uuid', companyName: 'Test', email: 'test@test.local', isAdmin: false };
  const reqSess  = { headers: {}, cookies: {}, session: { user: mockUser } };
  let nextCalled = false;
  auth.middleware(reqSess, {}, () => { nextCalled = true; });
  test('session user: next() called',  nextCalled,              true);
  test('session user: req.user set',   !!reqSess.user,          true);
  test('session user: clientId match', reqSess.user.clientId,   'WZ01325');

  // ── Admin user из session ──
  const adminUser = { clientId: 'WZ01112', isAdmin: true };
  const reqAdmin  = { headers: {}, cookies: {}, session: { user: adminUser } };
  let adminNext = false;
  auth.middleware(reqAdmin, {}, () => { adminNext = true; });
  test('admin session: next() called', adminNext,              true);
  test('admin session: isAdmin=true',  reqAdmin.user.isAdmin,  true);

  // ── Нет аутентификации → редирект ──
  let redirectUrl = null;
  const reqNone = { headers: {}, cookies: {}, session: {}, method: 'GET', originalUrl: '/some-page' };
  auth.middleware(reqNone, { redirect: (url) => { redirectUrl = url; } }, () => {});
  test('no auth: redirected to /login', redirectUrl !== null && redirectUrl.startsWith('/login'), true);

  // ── requireAdmin: admin проходит ──
  let adminPassed = false;
  requireAdmin({ user: { isAdmin: true } }, {}, () => { adminPassed = true; });
  test('requireAdmin: admin passes', adminPassed, true);

  // ── requireAdmin: не-admin блокируется ──
  let blockedStatus = null;
  requireAdmin({ user: { isAdmin: false } }, {
    status: (s) => { blockedStatus = s; return { send: () => {} }; },
  }, () => {});
  test('requireAdmin: non-admin blocked (403)', blockedStatus, 403);

} catch (e) {
  console.error('auth.js FATAL:', e.message);
  fail++;
}

// ────────────────────────────────────────────────────────────────
// 5. safeReturn — защита от open redirect (A01)
// ────────────────────────────────────────────────────────────────
section('safeReturn (open redirect protection)');

test('normal path allowed',          safeReturn('/dashboard'),          '/dashboard');
test('root / allowed',               safeReturn('/'),                   '/');
test('path with query allowed',      safeReturn('/edit?id=5'),          '/edit?id=5');
test('//evil.com blocked',           safeReturn('//evil.com'),          '/');
test('https://evil.com blocked',     safeReturn('https://evil.com'),    '/');
test('http://evil.com blocked',      safeReturn('http://evil.com'),     '/');
test('undefined → /',                safeReturn(undefined),             '/');
test('null → /',                     safeReturn(null),                  '/');
test('empty string → /',             safeReturn(''),                    '/');
test('//ok.com/path blocked',        safeReturn('//ok.com/path'),       '/');
test('nested path allowed',          safeReturn('/admin/users'),        '/admin/users');

// ────────────────────────────────────────────────────────────────
// 6. CSRF middleware — getSessionIdentifier использует req.sessionID
// ────────────────────────────────────────────────────────────────
section('CSRF getSessionIdentifier');

// Проверяем что csrf.js экспортирует объект с initCsrf
test('csrf module exports initCsrf', typeof csrfMiddleware.initCsrf, 'function');

// Проверяем поведение getSessionIdentifier через тестовые значения
// Функция в модуле читает req.sessionID (не req.cookies.jwt)
const { doubleCsrfProtection: dp, generateCsrfToken: gct } = csrfMiddleware.initCsrf();
test('initCsrf returns doubleCsrfProtection fn', typeof dp,  'function');
test('initCsrf returns generateCsrfToken fn',    typeof gct, 'function');



// ────────────────────────────────────────────────────────────────
// Итог
// ────────────────────────────────────────────────────────────────
console.log('\n══════════════════════════════');
console.log(pass + ' PASS, ' + fail + ' FAIL');
console.log('══════════════════════════════');
process.exit(fail > 0 ? 1 : 0);

})().catch(e => {
  console.error('\nTest runner fatal error:', e.message);
  process.exit(1);
});
