import assert from 'node:assert/strict';
import { config } from '../src/config/index.js';
import { healthTracker } from '../src/services/healthTracker.js';
import { supabaseService } from '../src/services/SupabaseService.js';
import {
    ReconciliationParityHeartbeatService
} from '../src/services/reconciliationParityHeartbeatService.js';

type MutableConfig = typeof config & Record<string, any>;
const mutableConfig = config as MutableConfig;

const restoreConfig = (snapshot: Record<string, unknown>) => {
    for (const [key, value] of Object.entries(snapshot)) {
        mutableConfig[key] = value;
    }
};

const resetTradingControl = () => {
    healthTracker.recordTradingControl({
        mode: 'RUNNING',
        lastChangedBy: 'test',
        lastChangedAt: Date.now(),
        reason: 'reset'
    });
};

const createMockExecutor = () => {
    const reconcileCalls: any[] = [];
    return {
        executor: {
            fetchExchangePosition: async () => null,
            reconcileExitFill: async (...args: any[]) => {
                reconcileCalls.push(args);
            }
        } as any,
        reconcileCalls
    };
};

const withPatchedSupabase = async (run: (calls: {
    logOrderCalls: any[];
    existingOrderIds: Set<string>;
}) => Promise<void>) => {
    const logOrderCalls: any[] = [];
    const existingOrderIds = new Set<string>();

    const originalGetProfileCapital = supabaseService.getProfileCapital.bind(supabaseService);
    const originalGetVirtualOpenPosition = supabaseService.getVirtualOpenPosition.bind(supabaseService);
    const originalGetVirtualOpenPositionForTrade = supabaseService.getVirtualOpenPositionForTrade.bind(supabaseService);
    const originalHasLifecycleEntryOrder = supabaseService.hasLifecycleEntryOrder.bind(supabaseService);
    const originalHasLifecycleEntryOrderWithProfileSubTag = supabaseService.hasLifecycleEntryOrderWithProfileSubTag.bind(supabaseService);
    const originalGetExistingOrderIds = supabaseService.getExistingOrderIds.bind(supabaseService);
    const originalLogOrder = supabaseService.logOrder.bind(supabaseService);

    try {
        (supabaseService as any).getProfileCapital = async () => ({
            allocatedCapital: 1000,
            isActive: true,
            userId: 'user-1'
        });
        (supabaseService as any).getVirtualOpenPosition = async (_profileId: string, symbol: string) => {
            if (symbol === 'BTC/USDT') {
                return {
                    profileId: 'profile-1',
                    symbol,
                    side: 'BUY',
                    qty: 1,
                    entryPrice: 100,
                    stopLoss: 95,
                    takeProfit: 120,
                    userId: 'user-1',
                    tradeId: 'trade-1',
                    tradeIds: ['trade-1']
                };
            }
            return null;
        };
        (supabaseService as any).getVirtualOpenPositionForTrade = async (_profileId: string, symbol: string, tradeId: string) => {
            if (symbol === 'BTC/USDT' && tradeId === 'trade-1') {
                return {
                    profileId: 'profile-1',
                    symbol,
                    side: 'BUY',
                    qty: 1,
                    entryPrice: 100,
                    stopLoss: 95,
                    takeProfit: 120,
                    userId: 'user-1',
                    tradeId,
                    tradeIds: [tradeId]
                };
            }
            return null;
        };
        (supabaseService as any).hasLifecycleEntryOrder = async () => true;
        (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => true;
        (supabaseService as any).getExistingOrderIds = async (orderIds: string[]) => {
            const out = new Set<string>();
            for (const id of orderIds || []) {
                if (existingOrderIds.has(String(id))) out.add(String(id));
            }
            return out;
        };
        (supabaseService as any).logOrder = async (payload: any) => {
            logOrderCalls.push(payload);
            existingOrderIds.add(String(payload?.order_id || ''));
        };

        await run({ logOrderCalls, existingOrderIds });
    } finally {
        (supabaseService as any).getProfileCapital = originalGetProfileCapital;
        (supabaseService as any).getVirtualOpenPosition = originalGetVirtualOpenPosition;
        (supabaseService as any).getVirtualOpenPositionForTrade = originalGetVirtualOpenPositionForTrade;
        (supabaseService as any).hasLifecycleEntryOrder = originalHasLifecycleEntryOrder;
        (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = originalHasLifecycleEntryOrderWithProfileSubTag;
        (supabaseService as any).getExistingOrderIds = originalGetExistingOrderIds;
        (supabaseService as any).logOrder = originalLogOrder;
    }
};

const testStreakAndIdempotency = async () => {
    const service = new ReconciliationParityHeartbeatService();
    const { executor, reconcileCalls } = createMockExecutor();

    await withPatchedSupabase(async ({ logOrderCalls }) => {
        const r1 = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });
        const r2 = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });
        const r3 = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });
        const r4 = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });

        assert.equal(r1.autoClosedTrades, 0);
        assert.equal(r2.autoClosedTrades, 0);
        assert.equal(r3.autoClosedTrades, 1, 'Third parity confirmation should auto-close once.');
        assert.equal(r4.autoClosedTrades, 0, 'No duplicate close expected after idempotent synthetic row exists.');
        assert.equal(r3.totalMismatchNotionalUsd, 100, 'Mismatch notional should capture the open trade exposure.');
        assert.equal(logOrderCalls.length, 1, 'Synthetic close must be logged once.');
        assert.equal(reconcileCalls.length, 1, 'Reconcile exit fill should run once.');
    });
};

const testSubTagQuarantine = async () => {
    const service = new ReconciliationParityHeartbeatService();
    const { executor, reconcileCalls } = createMockExecutor();

    await withPatchedSupabase(async ({ logOrderCalls }) => {
        (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => false;
        (supabaseService as any).hasLifecycleEntryOrder = async () => false;
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });
        assert.equal(result.autoClosedTrades, 0, 'Unattributed trades must never auto-close.');
        assert.equal(result.quarantinedTrades, 1, 'Unattributed trades should be quarantined.');
        assert.equal(result.totalMismatchNotionalUsd, 0, 'Unattributed quarantined trades should not contribute to watchdog mismatch notional.');
        assert.equal(logOrderCalls.length, 0, 'No synthetic close rows should be logged.');
        assert.equal(reconcileCalls.length, 0, 'No exit fill should be applied for quarantined trades.');
    });
};

const testLegacyAttributionFallback = async () => {
    const service = new ReconciliationParityHeartbeatService();
    const { executor, reconcileCalls } = createMockExecutor();

    await withPatchedSupabase(async ({ logOrderCalls }) => {
        (supabaseService as any).hasLifecycleEntryOrderWithProfileSubTag = async () => false;
        (supabaseService as any).hasLifecycleEntryOrder = async () => true;
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });
        assert.equal(result.quarantinedTrades, 0, 'Legacy fallback should keep attributable trades actionable.');
        assert.equal(result.totalMismatchNotionalUsd, 100, 'Legacy-attributed trades should still count for mismatch notional.');
        assert.equal(result.autoClosedTrades, 0, 'Single confirmation should not auto-close immediately.');
        assert.equal(logOrderCalls.length, 0, 'No synthetic close should be logged on first confirmation.');
        assert.equal(reconcileCalls.length, 0, 'No exit fill should be applied on first confirmation.');
    });
};

const testWatchdogPause = async () => {
    const service = new ReconciliationParityHeartbeatService();
    const { executor } = createMockExecutor();
    resetTradingControl();

    await withPatchedSupabase(async () => {
        (supabaseService as any).getProfileCapital = async () => ({
            allocatedCapital: 1000,
            isActive: true,
            userId: 'user-1'
        });
        (supabaseService as any).getVirtualOpenPosition = async (_profileId: string, symbol: string) => {
            if (symbol !== 'BTC/USDT') return null;
            return {
                profileId: 'profile-1',
                symbol,
                side: 'BUY',
                qty: 10,
                entryPrice: 100,
                stopLoss: 95,
                takeProfit: 120,
                userId: 'user-1',
                tradeId: 'trade-watchdog',
                tradeIds: ['trade-watchdog']
            };
        };
        (supabaseService as any).getVirtualOpenPositionForTrade = async (_profileId: string, symbol: string, tradeId: string) => ({
            profileId: 'profile-1',
            symbol,
            side: 'BUY',
            qty: 10,
            entryPrice: 100,
            stopLoss: 95,
            takeProfit: 120,
            userId: 'user-1',
            tradeId,
            tradeIds: [tradeId]
        });

        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor,
            monitoredSymbols: ['BTC/USDT']
        });

        assert.equal(result.integrityWatchdogTriggered, true, 'Watchdog must trigger for large mismatch notional.');
        assert.equal(result.totalMismatchNotionalUsd, 1000, 'Watchdog test should report cumulative mismatch notional.');
        assert.equal(healthTracker.isPaused(), true, 'Trading should be paused by watchdog for high-risk mismatch.');
    });
};

async function main() {
    const configSnapshot: Record<string, unknown> = {
        ENABLE_RECON_POSITION_PARITY_HEARTBEAT: mutableConfig.ENABLE_RECON_POSITION_PARITY_HEARTBEAT,
        RECON_POSITION_PARITY_DRY_RUN: mutableConfig.RECON_POSITION_PARITY_DRY_RUN,
        RECON_POSITION_PARITY_CONFIRMATIONS: mutableConfig.RECON_POSITION_PARITY_CONFIRMATIONS,
        RECON_POSITION_PARITY_DUST_ABS_QTY: mutableConfig.RECON_POSITION_PARITY_DUST_ABS_QTY,
        RECON_POSITION_PARITY_MAX_NOTIONAL_PCT: mutableConfig.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT,
        RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION: mutableConfig.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION,
        RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION: mutableConfig.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION,
        RECON_EXIT_BACKFILL_DUST_ABS_QTY: mutableConfig.RECON_EXIT_BACKFILL_DUST_ABS_QTY,
        RECON_EXIT_BACKFILL_DUST_REL_PCT: mutableConfig.RECON_EXIT_BACKFILL_DUST_REL_PCT,
        ENABLE_RECON_INTEGRITY_WATCHDOG: mutableConfig.ENABLE_RECON_INTEGRITY_WATCHDOG,
        RECON_INTEGRITY_WATCHDOG_THROTTLE_MS: mutableConfig.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS
    };

    try {
        mutableConfig.ENABLE_RECON_POSITION_PARITY_HEARTBEAT = true;
        mutableConfig.RECON_POSITION_PARITY_DRY_RUN = false;
        mutableConfig.RECON_POSITION_PARITY_CONFIRMATIONS = 3;
        mutableConfig.RECON_POSITION_PARITY_DUST_ABS_QTY = 0.0001;
        mutableConfig.RECON_POSITION_PARITY_MAX_NOTIONAL_PCT = 0.5;
        mutableConfig.RECON_POSITION_PARITY_REQUIRE_SUBTAG_ATTRIBUTION = true;
        mutableConfig.RECON_POSITION_PARITY_ALLOW_LEGACY_ENTRY_ATTRIBUTION = true;
        mutableConfig.RECON_EXIT_BACKFILL_DUST_ABS_QTY = 0.0001;
        mutableConfig.RECON_EXIT_BACKFILL_DUST_REL_PCT = 0;
        mutableConfig.ENABLE_RECON_INTEGRITY_WATCHDOG = true;
        mutableConfig.RECON_INTEGRITY_WATCHDOG_THROTTLE_MS = 0;

        await testStreakAndIdempotency();
        await testSubTagQuarantine();
        await testLegacyAttributionFallback();
        await testWatchdogPause();
        console.log('[reconciliation-parity-heartbeat] OK: streak, idempotency, quarantine, and watchdog behaviors validated');
    } finally {
        restoreConfig(configSnapshot);
        resetTradingControl();
    }
}

main().catch((error) => {
    console.error('[reconciliation-parity-heartbeat] failed', error);
    process.exit(1);
});
