import assert from 'node:assert/strict';
import { config } from '../src/config/index.js';
import { supabaseService } from '../src/services/SupabaseService.js';
import {
    ReconciliationExitBackfillService
} from '../src/services/reconciliationExitBackfillService.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 buildLifecycleEntry = (timestampMs: number) => ({
    profile_id: 'profile-1',
    user_id: 'user-1',
    symbol: 'BTC/USDT',
    trade_id: 'TRD-profile-1-BTCUSDT-BUY-000001',
    side: 'BUY',
    action: 'ENTRY',
    qty: 1,
    quantity: 1,
    price: 65000,
    status: 'filled',
    timestamp: timestampMs,
    created_at: new Date(timestampMs).toISOString(),
    sub_tag: 'BL:PAPER:P9999999999:T999999999999:ENTRY'
});

const createMockExecutor = (closedOrders: any[], exchangePosition: any = null) => ({
    fetchExchangePosition: async () => exchangePosition,
    fetchExchangeClosedOrders: async () => closedOrders
}) as any;

const withPatchedSupabase = async (
    lifecycleRows: any[],
    run: (calls: {
        auditRows: any[];
        upsertRows: any[];
    }) => Promise<void>
) => {
    const auditRows: any[] = [];
    const upsertRows: any[] = [];

    const originalIsAuditAvailable = supabaseService.isReconciliationBackfillAuditAvailable.bind(supabaseService);
    const originalGetFilledLifecycleOrdersForProfile = supabaseService.getFilledLifecycleOrdersForProfile.bind(supabaseService);
    const originalGetOpenOrdersForProfile = supabaseService.getOpenOrdersForProfile.bind(supabaseService);
    const originalGetExistingOrderIds = supabaseService.getExistingOrderIds.bind(supabaseService);
    const originalInsertAuditRows = supabaseService.insertReconciliationBackfillAuditRows.bind(supabaseService);
    const originalUpsertBackfillOrders = supabaseService.upsertReconciliationBackfillOrders.bind(supabaseService);

    try {
        (supabaseService as any).isReconciliationBackfillAuditAvailable = async () => true;
        (supabaseService as any).getFilledLifecycleOrdersForProfile = async () => lifecycleRows;
        (supabaseService as any).getOpenOrdersForProfile = async () => [];
        (supabaseService as any).getExistingOrderIds = async () => new Set<string>();
        (supabaseService as any).insertReconciliationBackfillAuditRows = async (rows: any[]) => {
            auditRows.push(...(rows || []));
            return true;
        };
        (supabaseService as any).upsertReconciliationBackfillOrders = async (rows: any[]) => {
            upsertRows.push(...(rows || []));
            return true;
        };

        await run({ auditRows, upsertRows });
    } finally {
        (supabaseService as any).isReconciliationBackfillAuditAvailable = originalIsAuditAvailable;
        (supabaseService as any).getFilledLifecycleOrdersForProfile = originalGetFilledLifecycleOrdersForProfile;
        (supabaseService as any).getOpenOrdersForProfile = originalGetOpenOrdersForProfile;
        (supabaseService as any).getExistingOrderIds = originalGetExistingOrderIds;
        (supabaseService as any).insertReconciliationBackfillAuditRows = originalInsertAuditRows;
        (supabaseService as any).upsertReconciliationBackfillOrders = originalUpsertBackfillOrders;
    }
};

const testUnattributedEvidenceBlocked = async () => {
    const now = Date.now();
    const service = new ReconciliationExitBackfillService();
    const executor = createMockExecutor([
        {
            id: 'ex-001',
            symbol: 'BTC/USD',
            side: 'sell',
            status: 'filled',
            filled_qty: 1,
            filled_avg_price: 64000,
            filled_at: new Date(now + 60_000).toISOString(),
            client_order_id: '8f42d6d5-c35a-4c3f-9f5e-b2e3fdcbe0f7',
            sub_tag: null
        }
    ]);

    await withPatchedSupabase([buildLifecycleEntry(now)], async ({ upsertRows }) => {
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor
        });
        assert.equal(result.attempted, true);
        assert.equal(result.proposedRows, 0, 'Unattributed fills must not be proposed for auto-backfill.');
        assert.equal(result.insertedRows, 0);
        assert.equal(result.noGoTrades, 1);
        assert.equal(result.noGoReasonCounts.missing_fill_evidence_for_large_remainder, 1);
        assert.equal(result.noGoSamples.length, 1);
        assert.equal(result.noGoSamples[0].reason, 'missing_fill_evidence_for_large_remainder');
        assert.equal(upsertRows.length, 0, 'No synthetic EXIT rows should be inserted for unattributed fills.');
    });
};

const testAttributedClientOrderEvidenceAccepted = async () => {
    const now = Date.now();
    const service = new ReconciliationExitBackfillService();
    const tradeId = 'TRD-profile-1-BTCUSDT-BUY-000001';
    const executor = createMockExecutor([
        {
            id: 'ex-002',
            symbol: 'BTC/USD',
            side: 'sell',
            status: 'filled',
            filled_qty: 1,
            filled_avg_price: 64250,
            filled_at: new Date(now + 60_000).toISOString(),
            client_order_id: `bytelyst-profile-1-${tradeId}-exit`,
            sub_tag: null
        }
    ]);

    await withPatchedSupabase([buildLifecycleEntry(now)], async ({ upsertRows }) => {
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor
        });
        assert.equal(result.attempted, true);
        assert.equal(result.proposedRows, 1, 'Attributed client_order_id should produce one backfill candidate.');
        assert.equal(result.noGoTrades, 0);
        assert.deepEqual(result.noGoReasonCounts, {});
        assert.equal(result.noGoSamples.length, 0);
        assert.equal(upsertRows.length, 0, 'Dry-run mode must not write rows.');
    });
};

const testTemporalGateRejectsStaleEvidence = async () => {
    const now = Date.now();
    const service = new ReconciliationExitBackfillService();
    const tradeId = 'TRD-profile-1-BTCUSDT-BUY-000001';
    const executor = createMockExecutor([
        {
            id: 'ex-003',
            symbol: 'BTC/USD',
            side: 'sell',
            status: 'filled',
            filled_qty: 1,
            filled_avg_price: 64100,
            filled_at: new Date(now - (10 * 60_000)).toISOString(),
            client_order_id: `bytelyst-profile-1-${tradeId}-exit`,
            sub_tag: null
        }
    ]);

    await withPatchedSupabase([buildLifecycleEntry(now)], async ({ upsertRows }) => {
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor
        });
        assert.equal(result.attempted, true);
        assert.equal(result.proposedRows, 0, 'Stale fills before lifecycle timestamp must not be auto-assigned.');
        assert.equal(result.noGoTrades, 1);
        assert.equal(result.noGoReasonCounts.missing_fill_evidence_for_large_remainder, 1);
        assert.equal(result.noGoSamples.length, 1);
        assert.equal(result.noGoSamples[0].reason, 'missing_fill_evidence_for_large_remainder');
        assert.equal(upsertRows.length, 0, 'Dry-run mode must not write rows.');
    });
};

const testOpenExchangePositionIsAdvisoryNotNoGo = async () => {
    const now = Date.now();
    const service = new ReconciliationExitBackfillService();
    const executor = createMockExecutor([], { qty: 1 });

    await withPatchedSupabase([buildLifecycleEntry(now)], async ({ auditRows, upsertRows }) => {
        const result = await service.runProfile({
            profileId: 'profile-1',
            userId: 'user-1',
            executor
        });
        assert.equal(result.attempted, true);
        assert.equal(result.proposedRows, 0, 'Open exchange positions must not auto-propose backfill rows.');
        assert.equal(result.noGoTrades, 0, 'Open exchange positions should be advisory, not NO_GO.');
        assert.deepEqual(result.noGoReasonCounts, {}, 'Advisory blockers must not populate NO_GO reason counts.');
        assert.equal(result.noGoSamples.length, 0);
        assert.equal(upsertRows.length, 0, 'Dry-run mode must not write rows.');
        assert.ok(
            auditRows.some((row) => row?.decision === 'SKIP_ACTIVE_POSITION'),
            'Expected advisory audit entry for active exchange position blocker.'
        );
    });
};

async function main() {
    const snapshot: Record<string, unknown> = {
        ENABLE_RECON_EXIT_BACKFILL: mutableConfig.ENABLE_RECON_EXIT_BACKFILL,
        RECON_EXIT_BACKFILL_DRY_RUN: mutableConfig.RECON_EXIT_BACKFILL_DRY_RUN,
        RECON_EXIT_BACKFILL_REQUIRE_PAUSE: mutableConfig.RECON_EXIT_BACKFILL_REQUIRE_PAUSE,
        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,
        RECON_EXIT_BACKFILL_LOOKBACK_HOURS: mutableConfig.RECON_EXIT_BACKFILL_LOOKBACK_HOURS,
        RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION: mutableConfig.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION,
        RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH: mutableConfig.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH,
        RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES: mutableConfig.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES,
        RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE: mutableConfig.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE,
        RECON_ORDER_COVERAGE_MAX_FETCH_PAGES: mutableConfig.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES,
        SYMBOLS: mutableConfig.SYMBOLS
    };

    try {
        mutableConfig.ENABLE_RECON_EXIT_BACKFILL = true;
        mutableConfig.RECON_EXIT_BACKFILL_DRY_RUN = true;
        mutableConfig.RECON_EXIT_BACKFILL_REQUIRE_PAUSE = false;
        mutableConfig.RECON_EXIT_BACKFILL_DUST_ABS_QTY = 0.0001;
        mutableConfig.RECON_EXIT_BACKFILL_DUST_REL_PCT = 0;
        mutableConfig.RECON_EXIT_BACKFILL_LOOKBACK_HOURS = 72;
        mutableConfig.RECON_EXIT_BACKFILL_REQUIRE_STRONG_ATTRIBUTION = true;
        mutableConfig.RECON_EXIT_BACKFILL_ALLOW_HEURISTIC_MATCH = false;
        mutableConfig.RECON_EXIT_BACKFILL_FILL_AFTER_TRADE_GRACE_MINUTES = 1;
        mutableConfig.RECON_ORDER_COVERAGE_FETCH_LIMIT_PER_PAGE = 500;
        mutableConfig.RECON_ORDER_COVERAGE_MAX_FETCH_PAGES = 8;
        mutableConfig.SYMBOLS = ['BTC/USDT'];

        await testUnattributedEvidenceBlocked();
        await testAttributedClientOrderEvidenceAccepted();
        await testTemporalGateRejectsStaleEvidence();
        await testOpenExchangePositionIsAdvisoryNotNoGo();
        console.log('[reconciliation-exit-backfill-evidence-guard] OK: attribution and temporal evidence guards validated');
    } finally {
        restoreConfig(snapshot);
    }
}

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