import assert from 'node:assert/strict';
import { Indicators } from '../src/utils/indicators.js';
import { DirectionTracker, SignalType } from '../src/strategies/directionTracker.js';
import { TrendBiasRule } from '../src/strategies/rules/TrendBiasRule.js';
import { MomentumRule } from '../src/strategies/rules/MomentumRule.js';
import { ZoneRule } from '../src/strategies/rules/ZoneRule.js';
import { SessionRule } from '../src/strategies/rules/SessionRule.js';
import { EntryTriggerRule } from '../src/strategies/rules/EntryTriggerRule.js';
import { RiskManagementRule } from '../src/strategies/rules/RiskManagementRule.js';
import { AIAnalysisRule } from '../src/strategies/rules/AIAnalysisRule.js';
import { ProStrategyEngine } from '../src/strategies/ProStrategyEngine.js';
import { SignalDirection, type MarketContext, type RuleResult } from '../src/strategies/rules/types.js';
import { config } from '../src/config/index.js';
import { RiskEngine } from '../src/services/riskEngine.js';
import { AutoTrader } from '../src/services/AutoTrader.js';
import { ExecutionManager } from '../src/services/executionManager.js';
import { supabaseService } from '../src/services/SupabaseService.js';
import { metrics, MetricsService } from '../src/services/MetricsService.js';

const makeCandle = (timestamp: number, open: number, high: number, low: number, close: number, volume: number = 100) => ({
    timestamp,
    open,
    high,
    low,
    close,
    volume
});

const buildSeries = (count: number, start: number, step: number) => {
    const out: Array<ReturnType<typeof makeCandle>> = [];
    for (let i = 0; i < count; i++) {
        const price = start + (step * i);
        out.push(makeCandle(1700000000000 + (i * 3600000), price - 1, price + 2, price - 2, price, 100 + i));
    }
    return out;
};

const makeContext = (overrides: Partial<MarketContext> = {}): MarketContext => {
    const candles1h = overrides.candles1h || buildSeries(120, 100, 1);
    const candles4h = overrides.candles4h || buildSeries(320, 80, 1.5);
    const candles15m = overrides.candles15m || buildSeries(120, 102, 0.4);

    return {
        symbol: overrides.symbol || 'BTC/USD',
        candles4h,
        candles1h,
        candles15m,
        currentPrice: overrides.currentPrice ?? candles15m[candles15m.length - 1].close,
        change24h: overrides.change24h ?? 1.2,
        changeToday: overrides.changeToday ?? 0.5,
        session: overrides.session || 'LDN|NY',
        isMajorSession: overrides.isMajorSession ?? true,
        volatility: overrides.volatility || 'High',
        latestSignal: overrides.latestSignal || SignalDirection.NONE,
        ema20_1h: overrides.ema20_1h ?? 150,
        ema20_15m: overrides.ema20_15m ?? 149,
        ema50_4h: overrides.ema50_4h ?? 140,
        ema200_4h: overrides.ema200_4h ?? 120,
        rsi_1h: overrides.rsi_1h ?? 58,
        rsi_15m: overrides.rsi_15m ?? 56
    };
};

async function testIndicatorsAndDirectionTracker() {
    assert.equal(Indicators.calculateEMA([], 10), 0, 'EMA should handle empty arrays');
    assert.equal(Indicators.calculateRSI([1, 2, 3], 14), 50, 'RSI should return fallback for short series');
    assert.equal(Indicators.calculateATR([], 14), 0, 'ATR should return 0 when candles are insufficient');

    const atrCandles = buildSeries(20, 100, 1);
    const atr = Indicators.calculateATR(atrCandles, 14);
    assert(atr > 0, 'ATR should compute positive value for valid candles');

    const tracker = new DirectionTracker();
    const shortSeries = tracker.calculateDirection(buildSeries(10, 100, 0.2));
    assert.equal(shortSeries.signal, SignalType.NONE);
    assert.equal(shortSeries.changed, false);

    const fromCloses = (closes: number[]) => closes.map((close, idx) =>
        makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100)
    );

    const buySeries = tracker.calculateDirection(fromCloses([
        100, 99, 101, 100, 102, 101, 103, 102, 104, 103,
        105, 104, 106, 105, 107, 106, 108, 107, 109, 108,
        110, 109, 111, 110, 112, 111, 113, 112, 114, 113
    ]));
    assert.equal(buySeries.signal, SignalType.BUY);
    assert.equal(buySeries.changed, true);

    const duplicateBuy = tracker.calculateDirection(fromCloses([
        102, 101, 103, 102, 104, 103, 105, 104, 106, 105,
        107, 106, 108, 107, 109, 108, 110, 109, 111, 110,
        112, 111, 113, 112, 114, 113, 115, 114, 116, 115
    ]));
    assert.equal(duplicateBuy.signal, SignalType.BUY);
    assert.equal(duplicateBuy.changed, false, 'State machine should suppress duplicate signal transitions');

    const sellSeries = tracker.calculateDirection(fromCloses([
        200, 201, 199, 200, 198, 199, 197, 198, 196, 197,
        195, 196, 194, 195, 193, 194, 192, 193, 191, 192,
        190, 191, 189, 190, 188, 189, 187, 188, 186, 187
    ]));
    assert.equal(sellSeries.signal, SignalType.SELL);
    assert.equal(sellSeries.changed, true);
}

async function testRules() {
    const trendRule = new TrendBiasRule();
    const trendInvalid = await trendRule.check(makeContext(), { fastPeriod: 200, slowPeriod: 50 });
    assert.equal(trendInvalid.passed, false);

    const trendInsufficient = await trendRule.check(makeContext({ candles4h: buildSeries(10, 80, 1) }));
    assert.equal(trendInsufficient.passed, false);

    const trendBuy = await trendRule.check(makeContext({ currentPrice: 600, ema50_4h: 500, ema200_4h: 400 }));
    assert.equal(trendBuy.signal, SignalDirection.BUY);

    const trendSell = await trendRule.check(makeContext({ currentPrice: 60, candles4h: buildSeries(320, 500, -1) }));
    assert.equal(trendSell.signal, SignalDirection.SELL);

    const momentumRule = new MomentumRule();
    const momentumShort = await momentumRule.check(
        makeContext({ candles1h: buildSeries(5, 100, 1), candles15m: buildSeries(5, 100, 1) }),
        { timeframe: '1h', rsiPeriod: 20 }
    );
    assert.equal(momentumShort.passed, false);

    const momentumBuySeries = [
        100, 99, 101, 100, 102, 101, 103, 102, 104, 103,
        105, 104, 106, 105, 107, 106, 108, 107, 109, 108,
        110, 109, 111, 110, 112, 111, 113, 112, 114, 113,
        115, 114, 116, 115, 117, 116, 118, 117, 119, 118
    ].map((close, idx) => makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100));
    const momentumBuy = await momentumRule.check(makeContext({ isMajorSession: true, candles1h: momentumBuySeries }), { timeframe: '1h' });
    assert.equal(momentumBuy.signal, SignalDirection.BUY);

    const momentumSellSeries = [
        200, 201, 199, 200, 198, 199, 197, 198, 196, 197,
        195, 196, 194, 195, 193, 194, 192, 193, 191, 192,
        190, 191, 189, 190, 188, 189, 187, 188, 186, 187,
        185, 186, 184, 185, 183, 184, 182, 183, 181, 182
    ].map((close, idx) => makeCandle(1700000000000 + (idx * 60000), close - 1, close + 1, close - 2, close, 100));
    const momentumSell = await momentumRule.check(makeContext({ isMajorSession: false, candles1h: momentumSellSeries }), { timeframe: '1h' });
    assert.equal(momentumSell.signal, SignalDirection.SELL);

    const zoneRule = new ZoneRule();
    const zoneNoEma = await zoneRule.check(makeContext({ ema20_15m: 0 }), { timeframe: '15m' });
    assert.equal(zoneNoEma.passed, false);

    const zonePass = await zoneRule.check(makeContext({ currentPrice: 101, ema20_1h: 100 }), { timeframe: '1h', zonePercent: 2 });
    assert.equal(zonePass.passed, true);

    const zoneFail = await zoneRule.check(makeContext({ currentPrice: 110, ema20_1h: 100 }), { timeframe: '1h', zonePercent: 1 });
    assert.equal(zoneFail.passed, false);

    const sessionRule = new SessionRule();
    const sessionPass = await sessionRule.check(makeContext({ session: 'LDN|TOK', isMajorSession: true }), { sessions: 'LDN,NY' });
    assert.equal(sessionPass.passed, true);

    const sessionFail = await sessionRule.check(makeContext({ session: 'TOK', isMajorSession: false }), { sessions: 'NY' });
    assert.equal(sessionFail.passed, false);

    const sessionOff = await sessionRule.check(makeContext({ session: 'OFF', isMajorSession: false }));
    assert.equal(sessionOff.passed, false);

    const entryRule = new EntryTriggerRule();
    const entryShort = await entryRule.check(makeContext({ candles1h: buildSeries(2, 100, 1) }));
    assert.equal(entryShort.passed, false);

    const entryNoEma = await entryRule.check(makeContext({ ema20_1h: 0 }), { timeframe: '1h' });
    assert.equal(entryNoEma.passed, false);

    const bullishReclaimCtx = makeContext({
        candles1h: [
            makeCandle(1, 100, 101, 99, 99),
            makeCandle(2, 99, 101, 98, 98),
            makeCandle(3, 98, 105, 97, 104)
        ],
        ema20_1h: 100
    });
    const bullishReclaim = await entryRule.check(bullishReclaimCtx, { timeframe: '1h' });
    assert.equal(bullishReclaim.signal, SignalDirection.BUY);

    const bearishReclaimCtx = makeContext({
        candles1h: [
            makeCandle(1, 100, 105, 99, 104),
            makeCandle(2, 104, 106, 102, 105),
            makeCandle(3, 105, 106, 95, 96)
        ],
        ema20_1h: 100
    });
    const bearishReclaim = await entryRule.check(bearishReclaimCtx, { timeframe: '1h', enableEmaReclaim: true, enableWickRejection: false });
    assert.equal(bearishReclaim.signal, SignalDirection.SELL);

    const riskRule = new RiskManagementRule();
    const riskShort = await riskRule.check(makeContext({ candles1h: buildSeries(10, 100, 1) }));
    assert.equal(riskShort.passed, false);

    const riskPass = await riskRule.check(makeContext(), { maxRisk: 10 });
    assert.equal(riskPass.passed, true);

    const riskFail = await riskRule.check(makeContext({ currentPrice: 1, candles1h: buildSeries(120, 1, 1) }), { maxRisk: 0.1 });
    assert.equal(riskFail.passed, false);
}

async function testAIAnalysisRule() {
    const aiRule = new AIAnalysisRule();
    const aiRuleAsAny = aiRule as any;

    const originalFailOpen = config.AI.FAIL_OPEN;
    try {
        aiRuleAsAny.aiClient.generateAnalysis = async () => null;

        config.AI.FAIL_OPEN = true;
        const failOpenRes = await aiRule.check(makeContext());
        assert.equal(failOpenRes.passed, true);
        assert.equal(failOpenRes.metadata.ai_fail_open, true);

        config.AI.FAIL_OPEN = false;
        const failClosedRes = await aiRule.check(makeContext());
        assert.equal(failClosedRes.passed, false);

        aiRuleAsAny.aiClient.generateAnalysis = async () => '{"action":"BUY","confidence":0.82,"reasoning":"good"}';
        config.AI.FAIL_OPEN = true;
        const parseOk = await aiRule.check(makeContext(), { minConfidence: 70 });
        assert.equal(parseOk.passed, true);
        assert.equal(parseOk.signal, SignalDirection.BUY);

        const parsedInvalid = aiRuleAsAny.parseResponse('not-json');
        assert.equal(parsedInvalid.action, 'HOLD');

        const normalizedPct = aiRuleAsAny.normalizeConfidenceToPct(0.77);
        assert.equal(normalizedPct, 77);
        const normalizedClamped = aiRuleAsAny.normalizeConfidenceToPct(220);
        assert.equal(normalizedClamped, 100);

        const prompt = aiRuleAsAny.buildPrompt(makeContext());
        assert(prompt.includes('Analyze the following Crypto Market Data'));
    } finally {
        config.AI.FAIL_OPEN = originalFailOpen;
    }
}

async function testRiskEngine() {
    const engine = new RiskEngine();
    const noSignal = await engine.calculateRiskProfile('BTC/USD', SignalDirection.NONE, makeContext());
    assert.equal(noSignal, null);

    const risk = await engine.calculateRiskProfile(
        'BTC/USD',
        SignalDirection.BUY,
        makeContext(),
        {
            allocated_capital: 1000,
            risk_per_trade_percent: 1,
            strategy_config: {
                execution: {
                    minQty: 0.001,
                    maxQty: 5,
                    qtyPrecision: 3,
                    minNotionalUsd: 10,
                    maxNotionalUsd: 10000
                }
            }
        },
        500
    );

    assert(risk !== null);
    assert(risk!.positionSize > 0);

    const rejectMinQty = await engine.calculateRiskProfile(
        'BTC/USD',
        SignalDirection.BUY,
        makeContext({ currentPrice: 50000 }),
        {
            allocated_capital: 100,
            risk_per_trade_percent: 0.1,
            strategy_config: {
                execution: {
                    minQty: 5,
                    maxQty: 10,
                    qtyPrecision: 2,
                    minNotionalUsd: 10,
                    maxNotionalUsd: 1000
                }
            }
        },
        5
    );
    assert.equal(rejectMinQty, null);
}

async function testProStrategyEngine() {
    const fakeExchange = {
        async fetchOHLCV(_symbol: string, timeframe: string) {
            if (timeframe === '4h') return buildSeries(320, 100, 1.2);
            if (timeframe === '1h') return buildSeries(140, 120, 0.8);
            return buildSeries(140, 121, 0.2);
        }
    } as any;

    const engine = new ProStrategyEngine(fakeExchange);

    const context = await engine.buildMarketContext('BTC/USD');
    assert(context, 'buildMarketContext should return context with enough candles');

    const fail = await engine.evaluateContext(makeContext(), [
        { ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 200, slowPeriod: 50 } }
    ]);
    assert.equal(fail?.passed, false);

    const pass = await engine.evaluateContext(makeContext({
        currentPrice: 600,
        session: 'LDN|NY',
        isMajorSession: true
    }), [
        { ruleId: 'TrendBiasRule', enabled: true, params: { fastPeriod: 50, slowPeriod: 200 } },
        { ruleId: 'SessionRule', enabled: true, params: { sessions: 'LDN,NY' } },
        { ruleId: 'AIAnalysisRule', enabled: false, params: {} }
    ]);

    assert(pass, 'evaluateContext should return a result object');
    assert.equal(pass?.passed, true);
    assert(pass?.metadata?.ruleStatuses, 'Rule statuses should be included in pass response');
}

async function testAutoTrader() {
    const originalEnableTrading = config.ENABLE_TRADING;
    const originalMaxOpen = config.MAX_OPEN_TRADES;

    const originalDailyLoss = (supabaseService as any).getProfileDailyLossUsd;
    const originalConsecutive = (supabaseService as any).getProfileConsecutiveLosses;

    const closeCalls: Array<{ symbol: string; reason: string; tradeId?: string }> = [];
    const openCalls: Array<any> = [];

    const executor = {
        profileId: 'profile-1',
        positionsBySymbol: new Map<string, any[]>(),
        checkCooldown: () => false,
        getOpenPositionCount: () => 0,
        getProfileId() { return this.profileId; },
        getAllPositions: () => new Map<string, any>(),
        getActivePositions(symbol: string) { return this.positionsBySymbol.get(symbol) || []; },
        async closePosition(symbol: string, reason: string, tradeId?: string) {
            closeCalls.push({ symbol, reason, tradeId });
        },
        async openPosition(...args: any[]) {
            openCalls.push(args);
            return { success: true };
        }
    } as any;

    const exchange = {
        async getPosition() { return null; }
    } as any;

    const trader = new AutoTrader(executor, exchange, async () => ({ allowed: true }));
    const resultBuy: RuleResult = { ruleName: 'x', passed: true, signal: SignalDirection.BUY };
    const resultSell: RuleResult = { ruleName: 'x', passed: true, signal: SignalDirection.SELL };

    try {
        (supabaseService as any).getProfileDailyLossUsd = async () => 0;
        (supabaseService as any).getProfileConsecutiveLosses = async () => 0;

        config.ENABLE_TRADING = false;
        await trader.handleSignal('BTC/USD', resultBuy, makeContext());
        assert.equal(openCalls.length, 0);

        config.ENABLE_TRADING = true;

        await trader.handleSignal('BTC/USD', resultBuy, makeContext(), { symbols: 'ETH/USD' });
        assert.equal(openCalls.length, 0, 'Symbol filtering should skip symbols not in profile list');

        executor.positionsBySymbol.set('BTC/USD', [{ side: SignalDirection.BUY, entryPrice: 100, peakPrice: 100, tradeId: 't1' }]);
        await trader.handleSignal('BTC/USD', resultSell, makeContext({ currentPrice: 95 }), { strategy_config: { execution: { allowPyramiding: false } } });
        assert.equal(closeCalls.length, 1, 'Opposite signal should close existing position');

        executor.positionsBySymbol.clear();

        await trader.handleSignal('BTC/USD', resultSell, makeContext(), { strategy_config: { execution: { entryMode: 'long_only' } } });
        assert.equal(openCalls.length, 0, 'long_only should block SELL entries');

        const blockedTrader = new AutoTrader(executor, exchange, async () => ({ allowed: false, reason: 'blocked' }));
        await blockedTrader.handleSignal('BTC/USD', resultBuy, makeContext());
        assert.equal(openCalls.length, 0, 'Portfolio guard block should stop entries');

        const maxOpenExecutor = { ...executor, getOpenPositionCount: () => 999 } as any;
        const maxOpenTrader = new AutoTrader(maxOpenExecutor, exchange);
        config.MAX_OPEN_TRADES = 1;
        await maxOpenTrader.handleSignal('BTC/USD', resultBuy, makeContext());
        assert.equal(openCalls.length, 0, 'Max open trade guard should block entries');

        const cooldownExecutor = { ...executor, checkCooldown: () => true } as any;
        const cooldownTrader = new AutoTrader(cooldownExecutor, exchange);
        await cooldownTrader.handleSignal('BTC/USD', resultBuy, makeContext());
        assert.equal(openCalls.length, 0, 'Cooldown guard should block entries');

        (supabaseService as any).getProfileDailyLossUsd = async () => 500;
        const riskBlocked = new AutoTrader(executor, exchange);
        await riskBlocked.handleSignal('BTC/USD', resultBuy, makeContext(), {
            strategy_config: { riskLimits: { maxDailyLossUsd: 100, maxConsecutiveLosses: 5 } }
        });
        assert.equal(openCalls.length, 0, 'Runtime risk guard should block entries when daily loss breached');

        (supabaseService as any).getProfileDailyLossUsd = async () => 0;
        (supabaseService as any).getProfileConsecutiveLosses = async () => 0;

        await trader.handleSignal('BTC/USD', resultBuy, makeContext(), { user_id: 'user-1', allocated_capital: 1000, risk_per_trade_percent: 1 });
        assert.equal(openCalls.length, 1, 'Happy path should open new position');
    } finally {
        config.ENABLE_TRADING = originalEnableTrading;
        config.MAX_OPEN_TRADES = originalMaxOpen;
        (supabaseService as any).getProfileDailyLossUsd = originalDailyLoss;
        (supabaseService as any).getProfileConsecutiveLosses = originalConsecutive;
    }
}

async function testExecutionManagerAndMetrics() {
    const originalEnableTrading = config.ENABLE_TRADING;
    const originalAllowLegacy = process.env.ALLOW_LEGACY_EXECUTION_MANAGER;

    const fakeExchange = {
        async getPosition() { return null; },
        async placeOrder(_symbol: string, _side: 'buy' | 'sell', qty: number) {
            return { id: `ord-${qty}`, filled_avg_price: 123.45 };
        }
    } as any;

    const apiServerMock = {
        updateOrders: (_orders: any[]) => undefined,
        addHistory: (_trade: any) => undefined
    } as any;

    try {
        process.env.ALLOW_LEGACY_EXECUTION_MANAGER = 'true';
        config.ENABLE_TRADING = true;

        const manager = new ExecutionManager(fakeExchange, apiServerMock, 'global');
        const ctx = makeContext({ currentPrice: 120 });

        await manager.handleSignal('BTC/USD', { ruleName: 'r', passed: true, signal: SignalDirection.BUY }, ctx);
        assert.equal(manager.isSymbolLocked('BTC/USD'), true);

        await manager.handleSignal('BTC/USD', { ruleName: 'r', passed: true, signal: SignalDirection.SELL }, makeContext({ currentPrice: 130 }));
        assert.equal(manager.isSymbolLocked('BTC/USD'), false, 'Opposite signal should close and unlock symbol');

        await manager.executeManualTrade('ETH/USD', 'buy', 1, 'market', 125);
        assert.equal(manager.getActivePosition('ETH/USD')?.side, SignalDirection.BUY);

        manager.markTradeComplete('ETH/USD', 130, 'manual test close');
        assert.equal(manager.getActivePosition('ETH/USD'), null);

        const syncExchange = {
            async getPosition(symbol: string) {
                if (symbol.includes('SOL')) {
                    return { side: 'long', avg_entry_price: '50', qty: '2' };
                }
                return null;
            },
            async placeOrder() {
                return { id: 'dummy' };
            }
        } as any;

        const manager2 = new ExecutionManager(syncExchange, apiServerMock, 'global');
        await manager2.syncPositions(['SOL/USD', 'BTC/USD']);
        assert.equal(manager2.isSymbolLocked('SOL/USD'), true);
        assert.equal(manager2.isSymbolLocked('BTC/USD'), false);

        // Metrics singleton calls
        metrics.operationalEventsTotal.labels({
            severity: 'INFO',
            type: 'COVERAGE_TEST',
            profile_id: 'p1',
            symbol: 'BTC/USD'
        }).inc();
        const svc = MetricsService.getInstance();
        const exposition = await svc.getMetrics();
        assert(exposition.includes('bytelyst_bot_operational_events_total'));
        assert.equal(typeof svc.getContentType(), 'string');
    } finally {
        if (originalAllowLegacy === undefined) {
            delete process.env.ALLOW_LEGACY_EXECUTION_MANAGER;
        } else {
            process.env.ALLOW_LEGACY_EXECUTION_MANAGER = originalAllowLegacy;
        }
        config.ENABLE_TRADING = originalEnableTrading;
    }
}

async function run() {
    await testIndicatorsAndDirectionTracker();
    await testRules();
    await testAIAnalysisRule();
    await testRiskEngine();
    await testProStrategyEngine();
    await testAutoTrader();
    await testExecutionManagerAndMetrics();

    console.log('[core-module-coverage] OK: core strategy, risk, trader, and legacy execution paths validated');
}

await run();
