/**
 * verifyMarketDataEndpoints.ts
 *
 * Static contract checks for the dashboard market-data proxy endpoints.
 * These routes are thin upstream proxies, so the high-value regression
 * surface is auth, provider URL construction, normalization, and cache usage.
 */

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const apiServerSource = readFileSync(join(__dirname, 'src/services/apiServer.ts'), 'utf8');
const configSource = readFileSync(join(__dirname, 'src/config/index.ts'), 'utf8');

function escapeRegExp(value: string) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function assertRouteRequiresAuth(route: string) {
    const routePattern = new RegExp(
        `this\\.app\\.get\\('${escapeRegExp(route)}',\\s*this\\.requireAuth,`,
    );
    assert.match(
        apiServerSource,
        routePattern,
        `${route} must be registered with requireAuth middleware`,
    );
}

function assertSourceIncludes(fragment: string, message: string) {
    assert.ok(apiServerSource.includes(fragment), message);
}

function assertConfigIncludes(fragment: string, message: string) {
    assert.ok(configSource.includes(fragment), message);
}

function assertSourceMatches(pattern: RegExp, message: string) {
    assert.match(apiServerSource, pattern, message);
}

function testRoutesRequireAuth() {
    for (const route of [
        '/api/chart/bars',
        '/api/news',
        '/api/market/indices',
        '/api/research/profile',
        '/api/research/metrics',
        '/api/research/earnings',
        '/api/screener',
    ]) {
        assertRouteRequiresAuth(route);
    }

    console.log('[PASS] Market-data endpoints require authenticated requests.');
}

function testChartBarsContract() {
    assertSourceMatches(
        /this\.app\.get\('\/api\/chart\/bars'[\s\S]*if \(!symbol\)[\s\S]*status\(400\)\.json\(\{ error: 'symbol required' \}\)/,
        '/api/chart/bars must reject missing symbols',
    );
    assertSourceMatches(
        /this\.app\.get\('\/api\/chart\/bars'[\s\S]*getUserMarketDataAlpacaCredentials\(authUserId\)[\s\S]*if \(error instanceof MissingServiceConfigError\)[\s\S]*status\(503\)\.json\(\{ error: error\.message \}\)/,
        '/api/chart/bars must fail closed when Alpaca credentials are absent',
    );

    for (const [period, timeframe, limit] of [
        ['1D', '5Min', '100'],
        ['5D', '15Min', '200'],
        ['1M', '1Day', '31'],
        ['3M', '1Day', '92'],
        ['6M', '1Day', '183'],
        ['YTD', '1Day', '365'],
        ['1Y', '1Day', '365'],
        ['5Y', '1Week', '260'],
        ['MAX', '1Month', '240'],
    ]) {
        assertSourceMatches(
            new RegExp(`case '${period}':[\\s\\S]*timeframe = '${timeframe}';\\s*limit = ${limit};`),
            `/api/chart/bars must preserve ${period} -> ${timeframe}/${limit} mapping`,
        );
    }

    assertSourceIncludes(
        "symbol.includes('/')",
        '/api/chart/bars must detect crypto symbols separately from equities',
    );
    assertSourceIncludes(
        'https://data.alpaca.markets/v1beta3/crypto/us/bars?symbols=${encodedSymbol}&${qs.toString()}',
        '/api/chart/bars must call Alpaca crypto bars with symbols query',
    );
    assertSourceIncludes(
        'https://data.alpaca.markets/v2/stocks/${encodedSymbol}/bars?${qs.toString()}',
        '/api/chart/bars must call Alpaca stock bars endpoint',
    );
    assertSourceIncludes(
        "qs.set('feed', 'iex')",
        '/api/chart/bars stock requests must use IEX feed',
    );
    assertSourceIncludes(
        'rawBars = cryptoBars[symbol] ?? Object.values(cryptoBars)[0] ?? []',
        '/api/chart/bars must unwrap crypto keyed bar responses',
    );
    assertSourceMatches(
        /const bars = rawBars\.map[\s\S]*ts:\s+new Date\(b\.t\)\.getTime\(\)[\s\S]*open:\s+b\.o[\s\S]*high:\s+b\.h[\s\S]*low:\s+b\.l[\s\S]*close:\s+b\.c[\s\S]*volume:\s*b\.v/,
        '/api/chart/bars must normalize Alpaca bar fields for the web chart',
    );

    console.log('[PASS] /api/chart/bars contract is covered.');
}

function testAlpacaProxyContracts() {
    assertSourceMatches(
        /this\.app\.get\('\/api\/news'[\s\S]*Math\.max\(1, Math\.min\(50, Number\(req\.query\.limit\) \|\| 10\)\)/,
        '/api/news must clamp limit to 1..50',
    );
    assertSourceIncludes(
        'const NEWS_SYMBOL_PATTERN = /^[A-Z0-9][A-Z0-9./-]{0,15}$/;',
        '/api/news must constrain symbols to expected characters and length',
    );
    assertSourceIncludes(
        'const MAX_NEWS_SYMBOLS = 10;',
        '/api/news must limit the number of symbols sent upstream',
    );
    assertSourceMatches(
        /symbols = normalizeNewsSymbolsQuery\(req\.query\.symbols\)[\s\S]*status\(400\)\.json\(\{ error: validationError\.message \}\)/,
        '/api/news must reject invalid symbol queries before proxying to Alpaca',
    );
    assertSourceIncludes(
        'https://data.alpaca.markets/v1beta1/news?${qs.toString()}',
        '/api/news must proxy Alpaca news',
    );
    assertSourceIncludes("sort: 'desc'", '/api/news must request newest stories first');
    assertSourceIncludes(
        'res.json({ news: data.news ?? data })',
        '/api/news must normalize responses to a news array wrapper',
    );
    assertSourceIncludes(
        'https://data.alpaca.markets/v2/stocks/snapshots?symbols=SPY,DIA,QQQ&feed=iex',
        '/api/market/indices must request SPY/DIA/QQQ IEX snapshots',
    );
    assertSourceIncludes(
        'Alpaca snapshots fetch failed',
        '/api/market/indices must propagate Alpaca snapshot failures',
    );

    console.log('[PASS] Alpaca news and index proxy contracts are covered.');
}

function testFmpProxyContracts() {
    assertSourceIncludes(
        "import { fetchFmpJson, FmpFetchError } from './fmpCache.js';",
        'FMP routes must use the shared cache helper and typed upstream error',
    );
    assertSourceIncludes(
        'User FMP API key is required for research and screener endpoints',
        'FMP routes must surface an explicit per-user missing-key error',
    );

    const fetchFmpCalls = apiServerSource.match(/fetchFmpJson\(url\)/g) ?? [];
    assert.equal(fetchFmpCalls.length, 4, 'each FMP route must fetch through fetchFmpJson(url)');

    for (const [route, endpoint, errorMessage] of [
        ['/api/research/profile', 'https://financialmodelingprep.com/stable/profile?symbol=${encodeURIComponent(symbol)}&apikey=${apiKey}', 'FMP profile fetch failed'],
        ['/api/research/metrics', 'https://financialmodelingprep.com/stable/key-metrics?symbol=${encodeURIComponent(symbol)}&limit=4&apikey=${apiKey}', 'FMP metrics fetch failed'],
        ['/api/research/earnings', 'https://financialmodelingprep.com/stable/earnings-calendar?symbol=${encodeURIComponent(symbol)}&limit=8&apikey=${apiKey}', 'FMP earnings fetch failed'],
    ]) {
        assertSourceMatches(
            new RegExp(`this\\.app\\.get\\('${escapeRegExp(route)}'[\\s\\S]*if \\(!symbol\\)[\\s\\S]*status\\(400\\)\\.json\\(\\{ error: 'symbol required' \\}\\)`),
            `${route} must reject missing symbols`,
        );
        assertSourceIncludes(endpoint, `${route} must call the expected FMP endpoint`);
        assertSourceIncludes(errorMessage, `${route} must preserve its FMP upstream error message`);
    }

    assertSourceIncludes(
        'res.json(Array.isArray(data) ? data[0] ?? {} : data)',
        'profile and metrics endpoints must unwrap first FMP array result',
    );
    assertSourceIncludes(
        'res.json({ earnings: Array.isArray(data) ? data : [] })',
        '/api/research/earnings must normalize to an earnings array wrapper',
    );
    assertSourceIncludes(
        'https://financialmodelingprep.com/stable/company-screener?${qs.toString()}',
        '/api/screener must call FMP stable company-screener',
    );
    assertSourceIncludes(
        'const ALLOWED_SCREENER_SECTORS = new Set([',
        '/api/screener must keep an explicit sector allow-list',
    );
    assertSourceMatches(
        /if \(sector && sector !== 'All'\)[\s\S]*!ALLOWED_SCREENER_SECTORS\.has\(sector\)[\s\S]*status\(400\)\.json\(\{ error: 'Unsupported sector filter' \}\)[\s\S]*qs\.set\('sector', sector\)/,
        '/api/screener must reject unsupported sector values before building the FMP query',
    );
    assertSourceIncludes(
        "qs.set('isEtf',   'false')",
        '/api/screener must exclude ETFs by default',
    );
    assertSourceIncludes(
        'String(Math.min(100, Number(req.query.limit) || 50))',
        '/api/screener must cap result limit at 100',
    );
    assertSourceIncludes(
        'res.json({ results: Array.isArray(data) ? data : [] })',
        '/api/screener must normalize to a results array wrapper',
    );
    assertSourceIncludes(
        'FMP screener fetch failed',
        '/api/screener must preserve its FMP upstream error message',
    );

    console.log('[PASS] FMP research and screener proxy contracts are covered.');
}

function main() {
    testRoutesRequireAuth();
    testChartBarsContract();
    testAlpacaProxyContracts();
    testFmpProxyContracts();
    console.log('Market-data endpoint contract checks passed');
}

main();
