import assert from 'node:assert/strict';
import { webcrypto } from 'node:crypto';

let compareCalls = 0;
globalThis.window = {
    EnhancedSecureCryptoUtils: {
        constantTimeCompare(a, b) {
            compareCalls += 1;
            return a === b;
        }
    }
};

const { EnhancedSecureWebRTCManager } = await import('../src/network/EnhancedSecureWebRTCManager.js');

function createFakeManager() {
    const sent = [];
    return {
        sent,
        verificationCode: 'A1-B2-C3',
        sasValidationAttempts: 0,
        localVerificationConfirmed: false,
        remoteVerificationConfirmed: false,
        bothVerificationsConfirmed: false,
        disconnected: false,
        _validateSASCode: EnhancedSecureWebRTCManager.prototype._validateSASCode,
        _secureLog() {},
        deliverMessageToUI() {},
        disconnect() {
            this.disconnected = true;
        },
        dataChannel: {
            send(payload) {
                sent.push(JSON.parse(payload));
            }
        },
        _checkBothVerificationsConfirmed() {},
        processMessageQueue() {}
    };
}

function createSASManager() {
    return {
        _secureLog() {}
    };
}

function createVerificationReadinessManager({
    localDescription = { type: 'answer' },
    remoteDescription = { type: 'offer' },
    dataChannelState = 'connecting',
    verificationCode = 'A1-B2-C3',
    localFingerprint = 'AA:BB',
    remoteFingerprint = 'CC:DD'
} = {}) {
    const notifications = [];
    return {
        peerConnection: { localDescription, remoteDescription },
        dataChannel: { readyState: dataChannelState },
        verificationCode,
        _sasLocalFingerprint: localFingerprint,
        _sasRemoteFingerprint: remoteFingerprint,
        notifications,
        _isVerificationReady: EnhancedSecureWebRTCManager.prototype._isVerificationReady,
        onStatusChange(status) {
            notifications.push({ kind: 'status', value: status });
        },
        onVerificationRequired(code) {
            notifications.push({ kind: 'verification', value: code });
        }
    };
}

// testSASNormalization
{
    const manager = createFakeManager();
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, 'a1 b2 c3'), true);
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, 'A1B2C3'), true);
}

// testConstantTimeCompare
{
    const manager = createFakeManager();
    compareCalls = 0;
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, 'A1-B2-C3'), true);
    assert.equal(compareCalls, 1);
}

// testInvalidInputs
{
    const manager = createFakeManager();
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, null), false);
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, 'A1B2'), false);
    assert.equal(EnhancedSecureWebRTCManager.prototype._validateSASCode.call(manager, 'FFFFFF'), false);
}

// three failed attempts disconnect; a correct attempt signals only after validation
{
    const manager = createFakeManager();
    for (let i = 0; i < 2; i += 1) {
        assert.throws(
            () => EnhancedSecureWebRTCManager.prototype.confirmVerification.call(manager, 'FFFFFF'),
            /SAS_MISMATCH/
        );
    }
    assert.equal(manager.disconnected, false);
    assert.throws(
        () => EnhancedSecureWebRTCManager.prototype.confirmVerification.call(manager, 'FFFFFF'),
        /SAS_MAX_ATTEMPTS/
    );
    assert.equal(manager.disconnected, true);

    const validManager = createFakeManager();
    EnhancedSecureWebRTCManager.prototype.confirmVerification.call(validManager, 'a1 b2 c3');
    assert.equal(validManager.localVerificationConfirmed, true);
    assert.equal(validManager.sent[0].type, 'verification_confirmed');
    assert.equal(validManager.sent[0].data.verificationMethod, 'MANUAL_SAS_ENTRY');
}

// SAS is deterministic for the same key material and normalized fingerprints,
// and changes when either fingerprint changes.
{
    const manager = createSASManager();
    const keyMaterial = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
    const computeSAS = EnhancedSecureWebRTCManager.prototype._computeSAS;

    const baseline = await computeSAS.call(manager, keyMaterial, ' AA:BB ', 'CC:DD');
    const sameInputsNormalized = await computeSAS.call(manager, keyMaterial, 'aa:bb', ' cc:dd ');
    const changedLocal = await computeSAS.call(manager, keyMaterial, 'AA:BC', 'CC:DD');
    const changedRemote = await computeSAS.call(manager, keyMaterial, 'AA:BB', 'CC:DE');

    assert.equal(baseline, sameInputsNormalized);
    assert.notEqual(baseline, changedLocal);
    assert.notEqual(baseline, changedRemote);
}

// SAS rejects non-string or empty fingerprints instead of allowing JS coercion.
{
    const manager = createSASManager();
    const keyMaterial = new Uint8Array([1, 2, 3, 4]);
    const computeSAS = EnhancedSecureWebRTCManager.prototype._computeSAS;
    const invalidFingerprints = [{ fingerprint: 'aa' }, ['aa'], null, ''];

    for (const invalidFingerprint of invalidFingerprints) {
        await assert.rejects(
            () => computeSAS.call(manager, keyMaterial, invalidFingerprint, 'CC:DD'),
            /Security error: localFP must be a non-empty DTLS fingerprint string/
        );
        await assert.rejects(
            () => computeSAS.call(manager, keyMaterial, 'AA:BB', invalidFingerprint),
            /Security error: remoteFP must be a non-empty DTLS fingerprint string/
        );
    }
}

// The salt is built only from normalized fingerprint strings.
{
    const manager = createSASManager();
    const keyMaterial = new Uint8Array([9, 8, 7, 6]);
    let capturedSalt = '';
    const originalCryptoDescriptor = Object.getOwnPropertyDescriptor(globalThis, 'crypto');

    Object.defineProperty(globalThis, 'crypto', {
        configurable: true,
        value: {
            subtle: {
                importKey: (...args) => webcrypto.subtle.importKey(...args),
                deriveBits: async (params, ...args) => {
                    capturedSalt = new TextDecoder().decode(params.salt);
                    return webcrypto.subtle.deriveBits(params, ...args);
                }
            }
        }
    });

    try {
        await EnhancedSecureWebRTCManager.prototype._computeSAS.call(manager, keyMaterial, ' AA:BB ', 'CC:DD ');
        assert.equal(capturedSalt, 'webrtc-sas|aa:bb|cc:dd');
        assert.equal(capturedSalt.includes('[object Object]'), false);
    } finally {
        Object.defineProperty(globalThis, 'crypto', originalCryptoDescriptor);
    }
}

// Extraction returns a deterministic primary string for SAS binding.
{
    const manager = createSASManager();
    const sdp = [
        'v=0',
        'a=fingerprint:sha-512 FF:EE',
        'a=fingerprint:sha-256 BB:BB',
        'a=fingerprint:sha-256 AA:AA'
    ].join('\r\n');

    assert.equal(
        EnhancedSecureWebRTCManager.prototype._extractDTLSFingerprintFromSDP.call(manager, sdp),
        'AA:AA'
    );
}

// ICE diagnostics classify candidate types so connectivity failures are visible.
{
    const manager = createSASManager();
    const sdp = [
        'v=0',
        'a=candidate:1 1 UDP 2122252543 192.168.1.2 54400 typ host',
        'a=candidate:2 1 UDP 1686052607 203.0.113.10 40000 typ srflx raddr 192.168.1.2 rport 54400',
        'a=candidate:3 1 UDP 41819902 198.51.100.20 50000 typ relay raddr 0.0.0.0 rport 0',
        'a=candidate:4 1 UDP 1518280447 198.51.100.30 60000 typ prflx',
        'a=candidate:5 1 UDP 1518280447 198.51.100.40 61000 generation 0'
    ].join('\r\n');

    assert.deepEqual(
        EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP.call(manager, sdp),
        { total: 5, host: 1, srflx: 1, relay: 1, prflx: 1, unknown: 1 }
    );
}

// Manual exchange must not treat an ICE gathering timeout as completion.
{
    const listeners = new Map();
    const manager = {
        peerConnection: {
            iceGatheringState: 'gathering',
            addEventListener(eventName, handler) {
                listeners.set(eventName, handler);
            },
            removeEventListener(eventName) {
                listeners.delete(eventName);
            }
        }
    };

    // Both budgets are passed explicitly: gathering now waits past the soft
    // deadline while there is nothing at all to export (see
    // ice-gathering-patience.test.mjs), so relying on the default hard ceiling
    // here would stall this assertion for 25 s. The property under test is
    // unchanged — a timeout must report false, never "complete".
    assert.equal(
        await EnhancedSecureWebRTCManager.prototype.waitForIceGathering.call(manager, 0, 0),
        false
    );
}

// A timed-out ICE gathering can still yield usable candidates for manual export.
{
    const summary = EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP.call(
        createSASManager(),
        'a=candidate:1 1 UDP 2122252543 192.168.1.2 54400 typ host\r\n'
    );
    assert.equal(summary.total > 0, true);
}

// ICE gathering resolves positively only after the peer reports completion.
{
    let listener = null;
    const manager = {
        peerConnection: {
            iceGatheringState: 'gathering',
            addEventListener(_eventName, handler) {
                listener = handler;
            },
            removeEventListener() {}
        }
    };

    const gathering = EnhancedSecureWebRTCManager.prototype.waitForIceGathering.call(manager);
    manager.peerConnection.iceGatheringState = 'complete';
    listener();
    assert.equal(await gathering, true);
}

// ICE failure diagnostics summarize candidate-pair states without crashing.
{
    const reports = new Map([
        ['local-1', { id: 'local-1', type: 'local-candidate', candidateType: 'host', protocol: 'udp', address: '192.168.1.2', port: 5000 }],
        ['remote-1', { id: 'remote-1', type: 'remote-candidate', candidateType: 'srflx', protocol: 'udp', address: '203.0.113.10', port: 6000 }],
        ['pair-1', { id: 'pair-1', type: 'candidate-pair', state: 'failed', nominated: false, writable: false, bytesSent: 0, bytesReceived: 0, localCandidateId: 'local-1', remoteCandidateId: 'remote-1' }]
    ]);

    const manager = {
        peerConnection: {
            async getStats() {
                return reports;
            }
        }
    };

    assert.deepEqual(
        await EnhancedSecureWebRTCManager.prototype._collectIceFailureDiagnostics.call(manager),
        {
            pairCount: 1,
            states: { failed: 1 },
            pairs: [{
                state: 'failed',
                nominated: false,
                writable: false,
                bytesSent: 0,
                bytesReceived: 0,
                currentRoundTripTime: null,
                local: {
                    type: 'local-candidate',
                    candidateType: 'host',
                    protocol: 'udp',
                    address: '192.168.1.2',
                    port: 5000,
                    networkType: null
                },
                remote: {
                    type: 'remote-candidate',
                    candidateType: 'srflx',
                    protocol: 'udp',
                    address: '203.0.113.10',
                    port: 6000,
                    networkType: null
                }
            }]
        }
    );
}

// Remote SDP candidate summaries use the same parser as local diagnostics.
{
    const sdp = [
        'v=0',
        'a=candidate:1 1 UDP 2122252543 192.168.1.2 54400 typ host',
        'a=candidate:2 1 UDP 1686052607 203.0.113.10 40000 typ srflx'
    ].join('\r\n');
    assert.deepEqual(
        EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP.call(createSASManager(), sdp),
        { total: 2, host: 1, srflx: 1, relay: 0, prflx: 0, unknown: 0 }
    );
}

// ICE candidate details are redacted but still expose routing-relevant classes.
{
    const sdp = [
        'v=0',
        'a=candidate:1 1 UDP 2122252543 192.168.1.2 54400 typ host',
        'a=candidate:2 1 UDP 1686052607 203.0.113.10 40000 typ srflx',
        'a=candidate:3 1 TCP 1518280447 abcdef.local 9 typ host tcptype passive'
    ].join('\r\n');

    assert.deepEqual(
        EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP.call(createSASManager(), sdp),
        [
            { candidateType: 'host', protocol: 'udp', addressKind: 'private-ipv4', portPresent: true, tcpType: null },
            { candidateType: 'srflx', protocol: 'udp', addressKind: 'public-ipv4', portPresent: true, tcpType: null },
            { candidateType: 'host', protocol: 'tcp', addressKind: 'mdns', portPresent: true, tcpType: 'passive' }
        ]
    );
}

// ICE diagnostics include a copyable JSON string so browser logs do not hide
// candidate details behind collapsed DevTools objects.
{
    const logs = [];
    const originalConsoleInfo = console.info;
    console.info = (...args) => logs.push(args);
    try {
        const manager = {
            _summarizeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP,
            _describeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP,
            _logIceCandidateDiagnostics: EnhancedSecureWebRTCManager.prototype._logIceCandidateDiagnostics
        };
        manager._logIceCandidateDiagnostics('test candidates', 'a=candidate:1 1 UDP 1 192.168.1.2 5000 typ host', {
            signalingState: 'stable'
        });

        assert.equal(logs[0][0], '[SecureBit ICE] test candidates');
        assert.equal(typeof logs[0][1].candidateDetailsJson, 'string');
        assert.match(logs[0][1].candidateDetailsJson, /private-ipv4/);
        assert.equal(logs[0][1].signalingState, 'stable');
    } finally {
        console.info = originalConsoleInfo;
    }
}

// Remote mDNS-only candidates are surfaced as a user-visible TURN warning.
{
    const messages = [];
    const manager = {
        _secureLog() {},
        deliverMessageToUI(message, type) {
            messages.push({ message, type });
        },
        _summarizeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._summarizeIceCandidatesInSDP,
        _describeIceCandidatesInSDP: EnhancedSecureWebRTCManager.prototype._describeIceCandidatesInSDP,
        _hasOnlyMdnsHostCandidates: EnhancedSecureWebRTCManager.prototype._hasOnlyMdnsHostCandidates,
        _warnIfRemoteCandidatesNeedRelay: EnhancedSecureWebRTCManager.prototype._warnIfRemoteCandidatesNeedRelay
    };
    const mdnsOnlySdp = 'a=candidate:1 1 UDP 1 abcdef.local 5000 typ host';
    const srflxSdp = 'a=candidate:1 1 UDP 1 203.0.113.10 5000 typ srflx';

    assert.equal(manager._hasOnlyMdnsHostCandidates(mdnsOnlySdp), true);
    assert.equal(manager._warnIfRemoteCandidatesNeedRelay('answer', mdnsOnlySdp), true);
    assert.equal(messages[0].type, 'system');
    assert.match(messages[0].message, /TURN is configured/i);
    assert.equal(manager._hasOnlyMdnsHostCandidates(srflxSdp), false);
}

// Pending offer context preserves the creator's manual-exchange salt until the
// answer is applied, even if transient ICE/UI state temporarily loses it.
{
    const manager = {
        sessionSalt: Array.from({ length: 64 }, (_, index) => index),
        sessionId: 'session-a',
        connectionId: 'connection-a',
        keyFingerprint: 'AA:BB',
        _secureLog() {},
        _secureWipeMemory() {},
        _storePendingOfferContext: EnhancedSecureWebRTCManager.prototype._storePendingOfferContext,
        _restorePendingOfferContextIfNeeded: EnhancedSecureWebRTCManager.prototype._restorePendingOfferContextIfNeeded,
        _clearPendingOfferContext: EnhancedSecureWebRTCManager.prototype._clearPendingOfferContext
    };

    manager._storePendingOfferContext();
    manager.sessionSalt = null;
    manager.sessionId = null;
    manager.connectionId = null;
    manager.keyFingerprint = null;

    assert.equal(manager._restorePendingOfferContextIfNeeded(), true);
    assert.equal(manager.sessionSalt.length, 64);
    assert.equal(manager.sessionId, 'session-a');
    assert.equal(manager.connectionId, 'connection-a');
    assert.equal(manager.keyFingerprint, 'AA:BB');

    manager._clearPendingOfferContext();
    manager.sessionSalt = null;
    assert.equal(manager._restorePendingOfferContextIfNeeded(), false);
}

// Joining with an offer and generating an answer does not open verification
// before the answer has been applied by the creator and the channel opens.
{
    const joiner = createVerificationReadinessManager({
        dataChannelState: 'connecting'
    });
    assert.equal(EnhancedSecureWebRTCManager.prototype._isVerificationReady.call(joiner), false);
    assert.equal(
        EnhancedSecureWebRTCManager.prototype._notifyVerificationReadyIfPossible.call(joiner),
        false
    );
    assert.deepEqual(joiner.notifications, []);
}

// The creator has applied the answer only once both descriptions exist; even
// then verification waits for a real ready transport.
{
    const creatorBeforeAnswer = createVerificationReadinessManager({
        remoteDescription: null,
        dataChannelState: 'open'
    });
    assert.equal(EnhancedSecureWebRTCManager.prototype._isVerificationReady.call(creatorBeforeAnswer), false);

    const creatorAfterAnswerBeforeOpen = createVerificationReadinessManager({
        dataChannelState: 'connecting'
    });
    assert.equal(EnhancedSecureWebRTCManager.prototype._isVerificationReady.call(creatorAfterAnswerBeforeOpen), false);
}

// Verification opens only after negotiated descriptions, open data channel, and
// valid SAS fingerprint material are all present.
{
    const missingFingerprint = createVerificationReadinessManager({
        dataChannelState: 'open',
        remoteFingerprint: ''
    });
    assert.equal(EnhancedSecureWebRTCManager.prototype._isVerificationReady.call(missingFingerprint), false);

    const ready = createVerificationReadinessManager({
        dataChannelState: 'open'
    });
    assert.equal(EnhancedSecureWebRTCManager.prototype._isVerificationReady.call(ready), true);
    assert.equal(
        EnhancedSecureWebRTCManager.prototype._notifyVerificationReadyIfPossible.call(ready),
        true
    );
    assert.deepEqual(ready.notifications, [
        { kind: 'status', value: 'verifying' },
        { kind: 'verification', value: 'A1-B2-C3' }
    ]);

    // Existing happy path stays idempotent after the UI is opened once.
    EnhancedSecureWebRTCManager.prototype._notifyVerificationReadyIfPossible.call(ready);
    assert.equal(ready.notifications.length, 2);
}

// SDP diagnostics distinguish candidate-less exports from usable manual payloads.
{
    assert.equal(
        EnhancedSecureWebRTCManager.prototype._countIceCandidatesInSDP.call({}, 'v=0\r\na=mid:0'),
        0
    );
    assert.equal(
        EnhancedSecureWebRTCManager.prototype._countIceCandidatesInSDP.call(
            {},
            'v=0\r\na=candidate:1 1 udp 1 192.0.2.1 1234 typ host\r\na=candidate:2 1 udp 1 198.51.100.1 2345 typ srflx'
        ),
        2
    );
}

console.log('SAS verification tests passed');
