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

const root = __dirname;
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
const files = {
  bootstrap: read('app/bootstrap.php'),
  request: read('app/request.php'),
  security: read('app/security.php'),
  functions: read('app/functions.php'),
  login: read('login.php'),
  ticketView: read('ticket_view.php'),
  ticketCreate: read('ticket_create.php'),
  participantSearch: read('participant_search.php'),
  tickets: read('tickets.php'),
  appJs: read('assets/js/app.js'),
  style: read('assets/css/style.css'),
  header: read('app/header.php'),
  htaccess: read('.htaccess'),
  migration: read('20260823_security_performance_indexes.sql'),
  schema: read('baza.sql'),
  productionIgnore: read('.productionignore'),
};

let passed = 0;
let failed = 0;
function check(label, condition) {
  if (condition) {
    passed++;
    console.log('PASS | ' + label);
  } else {
    failed++;
    console.error('FAIL | ' + label);
  }
}

// Session hardening i proxy-aware transport.
const strictIndex = files.bootstrap.indexOf("'session.use_strict_mode' => '1'");
const cookieOnlyIndex = files.bootstrap.indexOf("'session.use_only_cookies' => '1'");
const transSidIndex = files.bootstrap.indexOf("'session.use_trans_sid' => '0'");
const sessionStartIndex = files.bootstrap.indexOf('session_start();');
check('Session strict mode se postavlja pre session_start', strictIndex >= 0 && strictIndex < sessionStartIndex);
check('Session koristi samo cookie pre session_start', cookieOnlyIndex >= 0 && cookieOnlyIndex < sessionStartIndex);
check('Session trans_sid je isključen pre session_start', transSidIndex >= 0 && transSidIndex < sessionStartIndex);
check('Secure cookie podržava HTTPS-only produkciju', files.bootstrap.includes("'secure' => $secureSessionCookie") && files.request.includes('APP_FORCE_SECURE_COOKIE'));
check('Forwarded zaglavlja se koriste samo za trusted proxy', /request_is_from_trusted_proxy\(\)[\s\S]*HTTP_X_FORWARDED_FOR/.test(files.request));
check('HTTPS iza trusted proxy-ja koristi X-Forwarded-Proto', files.request.includes('HTTP_X_FORWARDED_PROTO'));

// Persistent login rate limiting.
check('Session login_attempts mehanizam je uklonjen', !files.login.includes("$_SESSION['login_attempts']"));
check('Login rate limit koristi persistent login_attempts tabelu', files.security.includes('FROM login_attempts') && files.security.includes('INSERT INTO login_attempts'));
check('Email i IP se čuvaju kao HMAC hash', files.security.includes("hash_hmac('sha256'") && files.security.includes("login_rate_limit_hash('email'") && files.security.includes("login_rate_limit_hash('ip'"));
check('Politika ima 15 minuta, account 10 i IP 20 neuspeha', /'window_seconds'\s*=>\s*900/.test(files.security) && /'account_block_failures'\s*=>\s*10/.test(files.security) && /'ip_block_failures'\s*=>\s*20/.test(files.security));
check('Progresivni delay počinje nakon 5 neuspeha i ima gornju granicu', /'delay_after_failures'\s*=>\s*5/.test(files.security) && /'max_delay_seconds'\s*=>\s*3\.0/.test(files.security));
check('Blokada vraća HTTP 429 i Retry-After', files.login.includes('http_response_code(429)') && files.login.includes("header('Retry-After: '"));
check('Poruka blokade ne potvrđuje postojanje naloga', files.login.includes('Previše neuspešnih pokušaja. Sačekajte nekoliko minuta i pokušajte ponovo.'));
check('Neaktivan/nepostojeći nalog koristi dummy password proveru', files.login.includes('$dummyHash') && files.login.includes('password_verify'));
check('Brisanje session cookie-ja ne resetuje limiter', !files.security.includes('$_SESSION') && files.login.includes('login_rate_limit_state($email, $clientIp)'));
check('Uspeh se beleži persistentno pre kreiranja auth sesije', files.login.indexOf('login_rate_limit_record($email, $clientIp, true)') < files.login.indexOf('session_regenerate_id(true)'));
check('Security log ne prihvata password/cookie/session/token ključeve', /password\|cookie\|session\|csrf\|token\|secret/.test(files.security));

// Participant authorization.
const permissionBlock = files.functions.slice(files.functions.indexOf('function can_manage_ticket_participants'), files.functions.indexOf('function eligible_assignees'));
check('Participant dozvola uključuje sistem administratora', permissionBlock.includes('is_system_admin($user)'));
check('Participant dozvola uključuje podnosioca', permissionBlock.includes("$ticket['user_id']"));
check('Participant dozvola uključuje zaduženog korisnika', permissionBlock.includes("$ticket['assigned_to']"));
check('Participant dozvola uključuje odgovornu ulogu', permissionBlock.includes('user_has_role'));
check('Participant dozvola ne dobija pravo iz ticket_participants reda', !permissionBlock.includes('FROM ticket_participants') && !permissionBlock.includes('SELECT 1'));
check('add_participant POST ima authoritative 403 proveru', /\$action === 'add_participant'[\s\S]*?!\$canManageParticipants[\s\S]*?http_response_code\(403\)/.test(files.ticketView));
check('Forma za dodavanje učesnika je uslovno prikazana', /<\?php if \(\$canManageParticipants\): \?>[\s\S]*?participant-add-form/.test(files.ticketView));
check('Postojeći tiket prosleđuje ticket_id autocomplete endpoint-u', files.ticketView.includes("participant_search.php?ticket_id=' . $ticketId"));
check('Participant search učitava konkretan tiket', files.participantSearch.includes('FROM tickets WHERE id = ? LIMIT 1'));
check('Participant search koristi istu centralnu dozvolu', files.participantSearch.includes('can_manage_ticket_participants($ticket, $user)'));
check('Neovlašćen participant search vraća 403', /participant_search_forbidden[\s\S]*?http_response_code\(403\)/.test(files.participantSearch));
check('Create-ticket autocomplete ima CSRF-zaštićen poseban kontekst', files.ticketCreate.includes('participant_search.php?context=create&csrf_token=') && files.participantSearch.includes('csrf_token_is_valid($createCsrf)'));
check('Participant search vraća samo id, name i email', /SELECT id, name, email[\s\S]*LIMIT 10/.test(files.participantSearch));
check('Participant search zahteva najmanje 2 karaktera', /mb_strlen\(\$query\) < 2/.test(files.participantSearch));
check('JS ispravno dodaje q kada endpoint već ima query parametre', files.appJs.includes("endpoint.includes('?') ? '&' : '?'"));

// Server-side pagination.
check('Stari LIMIT 200 je uklonjen', !files.tickets.includes('LIMIT 200'));
check('Default pagination je 25', /\? \(int\)\$perPageRaw : 25/.test(files.tickets));
check('Dozvoljene per_page vrednosti su validirane', files.tickets.includes('$ticketPageSizes = [25, 50, 100]') && files.tickets.includes('in_array((int)$perPageRaw, $ticketPageSizes, true)'));
check('Page prihvata samo pozitivne ograničene cifre', files.tickets.includes('ctype_digit($pageRaw)') && files.tickets.includes('strlen($pageRaw) <= 9') && files.tickets.includes('(int)$pageRaw >= 1'));
check('COUNT i glavni upit dele isti WHERE', files.tickets.includes("$whereSql = implode(' AND ', $where)") && (files.tickets.match(/WHERE ' \. \$whereSql/g) || []).length >= 2);
check('COUNT koristi prepared statement', files.tickets.includes('$countStmt = db()->prepare($countSql)') && files.tickets.includes('$countStmt->execute($params)'));
check('LIMIT i OFFSET su bindovani kao integer', files.tickets.includes('$stmt->bindValue(count($params) + 1, $perPage, PDO::PARAM_INT)') && files.tickets.includes('$stmt->bindValue(count($params) + 2, $offset, PDO::PARAM_INT)'));
check('Prevelika stranica se svodi na poslednju', files.tickets.includes('$page = min($page, $totalPages)'));
check('Pagination linkovi čuvaju view/search/status/priority/per_page', ['view', 'q', 'status', 'priority', 'per_page'].every((key) => files.tickets.includes("'" + key + "' =>")));
check('Pagination koristi postojeće UI komponente', files.tickets.includes('class="pagination"') && files.tickets.includes('pagination_page_numbers'));
check('Pagination prikazuje opseg i ukupan broj', files.tickets.includes('pagination-summary') && files.tickets.includes('od <?= (int)$totalTickets ?> zahteva'));

function normalizePage(raw) {
  const value = String(raw ?? '');
  return /^\d+$/.test(value) && value.length <= 9 && Number(value) >= 1 ? Number(value) : 1;
}
check('Scenario page=0 ide na prvu stranicu', normalizePage('0') === 1);
check('Scenario page=-1 ide na prvu stranicu', normalizePage('-1') === 1);
check('Scenario nevalidan page ide na prvu stranicu', normalizePage('abc') === 1);
check('Scenario validna srednja stranica ostaje validna', normalizePage('7') === 7);
check('Scenario ekstremno velika page vrednost se odbacuje', normalizePage('999999999999999999') === 1);

// SQL i deployment zaštita.
check('Migracija kreira persistent login_attempts tabelu', files.migration.includes('CREATE TABLE IF NOT EXISTS `login_attempts`'));
check('Login tabela ima email/IP/time indekse', files.migration.includes('idx_login_email_success_time') && files.migration.includes('idx_login_ip_success_time') && files.migration.includes('idx_login_attempted_at'));
check('Postojeći participant unique ključ je prepoznat i nije dupliran', files.schema.includes('ADD PRIMARY KEY (`ticket_id`,`user_id`)') && !files.migration.includes('ALTER TABLE `ticket_participants`'));
check('Dodati su samo dokazani ticket comment/event kompozitni indeksi', files.migration.includes('idx_comments_ticket_created_id') && files.migration.includes('idx_events_ticket_created_id'));
check('Postojeći ključni indeksi ostaju prisutni', ['idx_tickets_responsible_role_status', 'idx_notifications_user_read_created', 'idx_equipment_user_status', 'uq_user_roles_user_role'].every((name) => files.schema.includes(name)));
check('QA/runtime fajlovi su blokirani kroz .htaccess', files.htaccess.includes('qa_.*') && files.htaccess.includes('test_.*') && files.htaccess.includes('runtime_.*') && files.htaccess.includes('Require all denied'));
check('SQL i MD fajlovi su blokirani kroz .htaccess', files.htaccess.includes('(?:sql|md|log)'));
check('Production exclude manifest izostavlja QA, SQL i razvojnu dokumentaciju', files.productionIgnore.includes('qa_*') && files.productionIgnore.includes('*.sql') && files.productionIgnore.includes('*.md'));

// Maintainability i regresija.
check('CSS ostaje jedan fajl i zadržava postojeće breakpoints', files.style.includes('@media (max-width: 1160px)') && files.style.includes('@media (max-width: 900px)') && files.style.includes('@media (max-width: 640px)'));
check('Globalni box-sizing više nije identično dupliran', (files.style.match(/\*\s*\{\s*box-sizing:\s*border-box;\s*\}/g) || []).length === 1);
check('Globalni smooth scroll više nije identično dupliran', (files.style.match(/html\s*\{\s*scroll-behavior:\s*smooth;\s*\}/g) || []).length === 1);
check('Asset cache verzija je 3.25.0', (files.header.match(/\?v=3\.25\.0/g) || []).length >= 2);

const opens = (files.style.match(/\{/g) || []).length;
const closes = (files.style.match(/\}/g) || []).length;
check('CSS vitičaste zagrade su izbalansirane', opens === closes);

console.log(`\nRESULT ${passed}/${passed + failed} PASS`);
process.exit(failed === 0 ? 0 : 1);
