/**
 * verifyApiContract.ts
 *
 * Static API contract verification for the trading backend.
 * Follows the same tsx-script pattern as verifyWebsocketContract.ts.
 *
 * Verifies without starting the server:
 *  1. Feature-flags response shape matches the shared contract
 *  2. TradeAuditEvent required fields are present
 *  3. BotState health subcontract (tradingControl shape)
 *  4. Shared realtime helper exports
 *  5. Feature-flag key constants match expected values
 */

import assert from 'node:assert/strict';
import {
    BACKTEST_FLAG_KEYS,
    TAB_FLAG_KEYS,
    type BacktestFeatureFlags,
    type TabFeatureFlags,
    type TradingFeatureFlagsResponse,
} from '../shared/feature-flags.js';
import {
    buildTradingSocketOptions,
    isUnauthorizedSocketError,
    SOCKET_NAMESPACES,
} from '../shared/realtime.js';
import { validateWebsocketContract } from './src/scripts/verifyWebsocketContract.js';
import type { BotState } from './src/services/apiServer.js';

// ---------------------------------------------------------------------------
// 1. Feature-flag key constants
// ---------------------------------------------------------------------------

function testFeatureFlagKeyConstants() {
    assert.equal(BACKTEST_FLAG_KEYS.ENABLE_BACKTEST, 'ENABLE_BACKTEST',
        'BACKTEST_FLAG_KEYS.ENABLE_BACKTEST must equal "ENABLE_BACKTEST"');
    assert.equal(BACKTEST_FLAG_KEYS.BACKTEST_CUSTOMER_ENABLED, 'BACKTEST_CUSTOMER_ENABLED',
        'BACKTEST_FLAG_KEYS.BACKTEST_CUSTOMER_ENABLED must equal "BACKTEST_CUSTOMER_ENABLED"');
    assert.equal(TAB_FLAG_KEYS.MARKETPLACE, 'TAB_MARKETPLACE_ENABLED',
        'TAB_FLAG_KEYS.MARKETPLACE must equal "TAB_MARKETPLACE_ENABLED"');
    assert.equal(TAB_FLAG_KEYS.MEMBERSHIP, 'TAB_MEMBERSHIP_ENABLED',
        'TAB_FLAG_KEYS.MEMBERSHIP must equal "TAB_MEMBERSHIP_ENABLED"');
    console.log('[PASS] Feature-flag key constants match shared contract.');
}

// ---------------------------------------------------------------------------
// 2. TradingFeatureFlagsResponse shape
// ---------------------------------------------------------------------------

function buildFeatureFlagsFixture(
    backtestOverrides?: Partial<BacktestFeatureFlags>,
    tabOverrides?: Partial<TabFeatureFlags>
): TradingFeatureFlagsResponse {
    return {
        backtest: {
            enableBacktest: false,
            customerEnabled: false,
            maxCsvBytes: 5_242_880,
            maxRows: 10_000,
            ...backtestOverrides,
        },
        tabs: {
            marketplace: true,
            membership: true,
            ...tabOverrides,
        },
    };
}

function testFeatureFlagsResponseShape() {
    const flags = buildFeatureFlagsFixture({ enableBacktest: true, customerEnabled: true });

    assert.ok('backtest' in flags, 'TradingFeatureFlagsResponse must have "backtest" key');
    assert.ok('tabs' in flags, 'TradingFeatureFlagsResponse must have "tabs" key');
    assert.ok(typeof flags.backtest.enableBacktest === 'boolean',
        'backtest.enableBacktest must be boolean');
    assert.ok(typeof flags.backtest.customerEnabled === 'boolean',
        'backtest.customerEnabled must be boolean');
    assert.ok(flags.backtest.maxCsvBytes === undefined || typeof flags.backtest.maxCsvBytes === 'number',
        'backtest.maxCsvBytes must be number or undefined');
    assert.ok(flags.backtest.maxRows === undefined || typeof flags.backtest.maxRows === 'number',
        'backtest.maxRows must be number or undefined');

    // Tab flags default to true (opt-out model)
    assert.equal(flags.tabs.marketplace, true,
        'tabs.marketplace must default to true');
    assert.equal(flags.tabs.membership, true,
        'tabs.membership must default to true');

    // Opt-out works correctly
    const restrictedFlags = buildFeatureFlagsFixture({}, { marketplace: false, membership: false });
    assert.equal(restrictedFlags.tabs.marketplace, false,
        'tabs.marketplace=false must disable the marketplace tab');
    assert.equal(restrictedFlags.tabs.membership, false,
        'tabs.membership=false must disable the membership tab');

    // Admin always sees backtest regardless of customerEnabled flag
    const adminFlags = buildFeatureFlagsFixture({ enableBacktest: true, customerEnabled: false });
    assert.equal(adminFlags.backtest.enableBacktest, true,
        'enableBacktest=true must be preserved in response');

    console.log('[PASS] TradingFeatureFlagsResponse shape matches shared contract.');
}

// ---------------------------------------------------------------------------
// 3. TradeAuditEvent shape (inline fixture — mirrors apiServer.ts interface)
// ---------------------------------------------------------------------------

interface TradeAuditEvent {
    event: string;
    userId?: string;
    profileId?: string;
    symbol?: string;
    outcome?: 'accepted' | 'rejected' | 'error';
    reason?: string;
    details?: Record<string, unknown>;
}

function testTradeAuditEventShape() {
    const validOutcomes = new Set<string>(['accepted', 'rejected', 'error']);

    const minimalEvent: TradeAuditEvent = { event: 'manual_order_created' };
    assert.ok(typeof minimalEvent.event === 'string' && minimalEvent.event.length > 0,
        'TradeAuditEvent.event must be a non-empty string');

    const fullEvent: TradeAuditEvent = {
        event: 'profile_control',
        userId: 'user-123',
        profileId: 'profile-abc',
        symbol: 'BTC/USDT',
        outcome: 'accepted',
        reason: 'within risk limits',
        details: { allocatedCapital: 1000, riskPerTrade: 1.5 },
    };

    assert.ok(fullEvent.outcome && validOutcomes.has(fullEvent.outcome),
        `TradeAuditEvent.outcome must be one of: ${[...validOutcomes].join(', ')}`);
    assert.ok(typeof fullEvent.details === 'object' && fullEvent.details !== null,
        'TradeAuditEvent.details must be a plain object when present');
    assert.ok(typeof fullEvent.details!.allocatedCapital === 'number',
        'TradeAuditEvent.details values must be serialisable');

    // Verify all three outcome literals are accepted
    for (const outcome of ['accepted', 'rejected', 'error'] as const) {
        const evt: TradeAuditEvent = { event: 'test', outcome };
        assert.ok(evt.outcome === outcome, `outcome literal "${outcome}" must round-trip`);
    }

    console.log('[PASS] TradeAuditEvent shape and outcome literals are correct.');
}

// ---------------------------------------------------------------------------
// 4. BotState health.tradingControl shape
// ---------------------------------------------------------------------------

function buildHealthFixture(mode: 'RUNNING' | 'PAUSED'): BotState['health'] {
    return {
        tradingLoopHealthy: true,
        tradingLoopLastRun: Date.now(),
        monitorLoopHealthy: true,
        monitorLoopLastRun: Date.now(),
        orderSyncHealthy: true,
        orderSyncLastRun: Date.now(),
        lockContentionCount: 0,
        reconciliationLoopHealthy: true,
        reconciliationLoopLastRun: Date.now(),
        reconciliationMismatchCount: 0,
        reconciliationMissingFromExchange: 0,
        reconciliationMissingInDb: 0,
        reconciliationNoGoTrades: 0,
        reconciliationNoGoReasonCounts: {},
        reconciliationNoGoSamples: [],
        reconciliationIntegrityWatchdogTriggered: false,
        reconciliationLockContentionCount: 0,
        tradingControl: {
            mode,
            lastChangedBy: 'system',
            lastChangedAt: Date.now(),
        },
    };
}

function testBotStateHealthShape() {
    const runningHealth = buildHealthFixture('RUNNING');
    assert.equal(runningHealth.tradingControl?.mode, 'RUNNING',
        'tradingControl.mode must be "RUNNING"');
    assert.ok(typeof runningHealth.tradingControl?.lastChangedBy === 'string',
        'tradingControl.lastChangedBy must be a string');
    assert.ok(typeof runningHealth.tradingControl?.lastChangedAt === 'number',
        'tradingControl.lastChangedAt must be a unix timestamp (number)');

    const pausedHealth = buildHealthFixture('PAUSED');
    assert.equal(pausedHealth.tradingControl?.mode, 'PAUSED',
        'tradingControl.mode must be "PAUSED"');

    assert.ok(typeof runningHealth.reconciliationNoGoReasonCounts === 'object',
        'reconciliationNoGoReasonCounts must be a plain object');
    assert.ok(Array.isArray(runningHealth.reconciliationNoGoSamples),
        'reconciliationNoGoSamples must be an array');

    console.log('[PASS] BotState health.tradingControl shape is correct.');
}

// ---------------------------------------------------------------------------
// 5. Shared realtime helpers
// ---------------------------------------------------------------------------

function testRealtimeHelpers() {
    const opts = buildTradingSocketOptions('test-token-abc');
    assert.ok(Array.isArray(opts.transports), 'buildTradingSocketOptions must return transports array');
    assert.ok(opts.transports.includes('websocket'), 'transports must include "websocket"');
    assert.ok(opts.transports.includes('polling'), 'transports must include "polling"');
    assert.deepEqual(opts.auth, { token: 'test-token-abc' }, 'auth.token must match input token');

    const optsWithPath = buildTradingSocketOptions('tok', '/custom/socket');
    assert.equal((optsWithPath as any).path, '/custom/socket',
        'socketPath must be forwarded when provided');

    assert.equal(isUnauthorizedSocketError('Unauthorized: invalid token'), true,
        'isUnauthorizedSocketError must detect "unauthorized"');
    assert.equal(isUnauthorizedSocketError('Invalid token provided'), true,
        'isUnauthorizedSocketError must detect "invalid token"');
    assert.equal(isUnauthorizedSocketError('Connection timeout'), false,
        'isUnauthorizedSocketError must return false for unrelated errors');

    // Named namespace constants
    assert.equal(SOCKET_NAMESPACES.TRADING, '/trading',
        'SOCKET_NAMESPACES.TRADING must equal "/trading"');
    assert.equal(SOCKET_NAMESPACES.ADMIN, '/admin',
        'SOCKET_NAMESPACES.ADMIN must equal "/admin"');

    console.log('[PASS] Shared realtime helper contracts are correct.');
}

// ---------------------------------------------------------------------------
// 6. WebSocket BotState contract (re-run verifyWebsocketContract fixture)
// ---------------------------------------------------------------------------

function testWebsocketBotStateContract() {
    const minimalState: BotState = {
        symbols: {},
        alerts: [],
        positions: [],
        orders: [],
        history: [],
        settings: {
            executionMode: 'Pro',
            riskPerTrade: 1,
            totalCapital: 10_000,
            maxOpenTrades: 3,
            isAlgoEnabled: false,
            enabledRules: [],
        },
        health: buildHealthFixture('RUNNING'),
        uptime: 0,
        accountSnapshot: null,
        orderFailures: [],
        operationalEvents: [],
    };

    const errors = validateWebsocketContract(minimalState);
    assert.equal(errors.length, 0,
        `Minimal BotState must pass WebSocket contract. Violations:\n${errors.join('\n')}`);

    console.log('[PASS] Minimal BotState passes WebSocket contract validation.');
}

// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------

function main() {
    testFeatureFlagKeyConstants();
    testFeatureFlagsResponseShape();
    testTradeAuditEventShape();
    testBotStateHealthShape();
    testRealtimeHelpers();
    testWebsocketBotStateContract();

    console.log('\n[PASS] All API contract checks passed.');
}

main();
