import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { readdir, readFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const frontendRoot = fileURLToPath(new URL('..', import.meta.url));
const sourceExtensions = new Set(['.html', '.js', '.mjs', '.css']);

async function sourceFiles(directory = frontendRoot) {
    const result = [];
    for (const entry of await readdir(directory, { withFileTypes: true })) {
        if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === 'tests') continue;
        const path = join(directory, entry.name);
        if (entry.isDirectory()) {
            result.push(...await sourceFiles(path));
        } else if (sourceExtensions.has(extname(entry.name))) {
            result.push(path);
        }
    }
    return result;
}

async function sourceCorpus() {
    const files = await sourceFiles();
    return Promise.all(files.map(async path => ({ path, text: await readFile(path, 'utf8') })));
}

test('frontend не исполняет удалённые или inline-скрипты', async () => {
    const corpus = await sourceCorpus();
    for (const { path, text } of corpus) {
        assert.doesNotMatch(text, /(?:from\s+|import\s*\()\s*['"]https?:\/\//, path);
        if (extname(path) === '.html') {
            assert.doesNotMatch(text, /<script\b(?![^>]*\bsrc=)[^>]*>/i, path);
            assert.doesNotMatch(text, /<style\b/i, path);
            assert.doesNotMatch(text, /\son[a-z]+\s*=/i, path);
        }
    }
});

test('access JWT и профиль сессии не читаются и не записываются в Web Storage', async () => {
    const corpus = await sourceCorpus();
    const authStorage = /(?:localStorage|sessionStorage)\.(?:getItem|setItem)\(\s*['"](?:token|role|departmentId|userId)['"]/;
    for (const { path, text } of corpus) {
        assert.doesNotMatch(text, authStorage, path);
    }
});

test('CSP запрещает произвольные inline-ресурсы и покрывает style-атрибуты хэшами', async () => {
    const corpus = await sourceCorpus();
    const config = await readFile(join(frontendRoot, 'security.conf'), 'utf8');

    assert.match(config, /script-src 'self'/);
    assert.match(config, /script-src-attr 'none'/);
    assert.match(config, /style-src 'self'/);
    assert.match(config, /style-src-attr 'unsafe-hashes'/);
    assert.match(config, /connect-src 'self'/);
    assert.doesNotMatch(config, /'unsafe-inline'|'unsafe-eval'/);

    const expectedHashes = new Set();
    for (const { path, text } of corpus) {
        assert.doesNotMatch(text, /\.style\.(?:cssText|[A-Za-z-]+)\s*=|\.style\.setProperty|setAttribute\(\s*['"]style/i, path);
        for (const match of text.matchAll(/style="([^"]*)"/g)) {
            assert.doesNotMatch(match[1], /\$\{/, `Динамический style-атрибут в ${path}`);
            expectedHashes.add(`sha256-${createHash('sha256').update(match[1]).digest('base64')}`);
        }
    }

    const configuredHashes = new Set(
        Array.from(config.matchAll(/'((?:sha256)-[^']+)'/g), match => match[1])
    );
    assert.deepEqual(configuredHashes, expectedHashes);
});

test('Docker-образ включает локальный telemetry bundle и security headers', async () => {
    const dockerfile = await readFile(join(frontendRoot, 'Dockerfile'), 'utf8');
    const telemetry = await readFile(join(frontendRoot, 'telemetry.js'), 'utf8');
    assert.match(dockerfile, /npm ci/);
    assert.match(dockerfile, /npm run build:vendor/);
    assert.match(dockerfile, /scripts\/build-vendor\.mjs/);
    assert.match(dockerfile, /COPY --from=frontend-assets \/build\/dist\/vendor\//);
    assert.match(dockerfile, /magistr-security\.conf/);
    assert.match(telemetry, /import\('\/vendor\/otel\.js'\)/);
    assert.doesNotMatch(telemetry, /https?:\/\//);
});

test('сообщения об ошибках не вставляют текст исключения через innerHTML', async () => {
    const files = [
        'admin/js/main.js',
        'admin/settings/js/main.js',
        'admin/settings/js/views/database.js',
        'admin/js/views/teacher-requests.js'
    ];

    for (const relativePath of files) {
        const source = await readFile(join(frontendRoot, relativePath), 'utf8');
        assert.doesNotMatch(source, /\$\{[^}\n]*(?:e|error|err)\.message[^}\n]*\}/, relativePath);
    }

    const utils = await readFile(join(frontendRoot, 'admin/js/utils.js'), 'utf8');
    assert.match(utils, /element\.textContent = message/);
    assert.match(utils, /container\.replaceChildren\(element\)/);
    assert.match(utils, /cell\.textContent = message/);
    assert.match(utils, /tbody\.replaceChildren\(row\)/);
});

test('пароли скрыты и помечены для менеджера паролей', async () => {
    const users = await readFile(join(frontendRoot, 'admin/views/users.html'), 'utf8');
    const database = await readFile(join(frontendRoot, 'admin/settings/views/database.html'), 'utf8');
    const teacherRequests = await readFile(join(frontendRoot, 'admin/js/views/teacher-requests.js'), 'utf8');

    assert.match(users, /id="new-password"[^>]*type="password"|type="password"[^>]*id="new-password"/);
    assert.match(users, /id="new-password"[^>]*autocomplete="new-password"/);
    assert.match(database, /id="tenant-password"[^>]*type="password"|type="password"[^>]*id="tenant-password"/);
    assert.match(database, /id="tenant-password"[^>]*autocomplete="new-password"/);
    assert.match(teacherRequests, /type="password"[^>]*class="teacher-request-password"[^>]*autocomplete="new-password"/);
});

test('tenant UI и production-логи соблюдают русский языковой регламент', async () => {
    const database = await readFile(join(frontendRoot, 'admin/settings/js/views/database.js'), 'utf8');
    assert.doesNotMatch(database, /\bOnline\b|\bOffline\b/);
    assert.match(database, /Доступно/);
    assert.match(database, /Недоступно/);

    const backendRoot = join(frontendRoot, '..', 'backend', 'src', 'main', 'java', 'com', 'magistr', 'app', 'config', 'tenant');
    const logSources = await Promise.all([
        'TenantRoutingDataSource.java',
        'TenantDataSourceConfig.java',
        'TenantInterceptor.java'
    ].map(name => readFile(join(backendRoot, name), 'utf8')));
    const tenantSources = logSources.flatMap(source =>
        Array.from(source.matchAll(/log\.(?:debug|info|warn|error)\("([^"]*)"/g), match => match[1])
    ).join('\n');
    assert.doesNotMatch(
        tenantSources,
        /Database API request|Unknown tenant|Resolved tenant|default DataSource|H2 in-memory|errorType|Tenant-БД|tenant-БД|startup lifecycle|startup Hikari pool/
    );

    const frontendSources = (await sourceCorpus())
        .filter(({ path }) => extname(path) === '.js')
        .map(({ text }) => text)
        .join('\n');
    assert.doesNotMatch(frontendSources, /console\.(?:log|info|warn|error)\([^\n]*(?:Failed|Error)/);
    assert.doesNotMatch(frontendSources, /console\.(?:warn|error)\([^\n]*\.message/);
});

test('единая матрица ролей согласует admin и settings для учебного отдела', async () => {
    const {
        ADMIN_APP_ROLES,
        SETTINGS_ROLES,
        capabilitiesForRole
    } = await import('../admin/js/role-capabilities.js');
    const educationOffice = capabilitiesForRole('EDUCATION_OFFICE');

    assert.ok(ADMIN_APP_ROLES.includes('EDUCATION_OFFICE'));
    assert.ok(SETTINGS_ROLES.includes('EDUCATION_OFFICE'));
    assert.ok(educationOffice.adminTabs.includes('academic-calendar'));
    assert.deepEqual(educationOffice.settingsTabs, ['time-slots', 'edu-forms']);
    assert.equal(educationOffice.defaultSettingsTab, 'time-slots');
    assert.equal(capabilitiesForRole('DEPARTMENT').settingsTabs.length, 0);

    const adminMain = await readFile(join(frontendRoot, 'admin/js/main.js'), 'utf8');
    const settingsMain = await readFile(join(frontendRoot, 'admin/settings/js/main.js'), 'utf8');
    assert.match(adminMain, /from '\.\/role-capabilities\.js'/);
    assert.match(settingsMain, /from '\.\.\/\.\.\/js\/role-capabilities\.js'/);
    assert.doesNotMatch(adminMain, /const ROLE_NAVIGATION/);
    assert.doesNotMatch(settingsMain, /const ROLE_TABS/);
});
