"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const sdk_1 = require("@bsv/sdk");
const sdk_2 = require("../sdk");
const CWIStyleWalletManager_1 = require("../CWIStyleWalletManager");
const globals_1 = require("@jest/globals");
const hash_wasm_1 = require("hash-wasm");
globals_1.jest.useFakeTimers();
// ------------------------------------------------------------------------------------------
// Mocks and Utilities
// ------------------------------------------------------------------------------------------
/** A utility to create an Outpoint string for test usage. */
function makeOutpoint(txid, vout) {
    return `${txid}:${vout}`;
}
/**
 * A mock underlying WalletInterface to verify that proxy methods:
 *  1. Are not callable if not authenticated
 *  2. Are disallowed if originator is admin
 *  3. Forward to the real method if conditions pass
 */
const mockUnderlyingWallet = {
    getPublicKey: globals_1.jest.fn(),
    revealCounterpartyKeyLinkage: globals_1.jest.fn(),
    revealSpecificKeyLinkage: globals_1.jest.fn(),
    encrypt: globals_1.jest.fn(),
    decrypt: globals_1.jest.fn(),
    createHmac: globals_1.jest.fn(),
    verifyHmac: globals_1.jest.fn(),
    createSignature: globals_1.jest.fn(),
    verifySignature: globals_1.jest.fn(),
    createAction: globals_1.jest.fn(),
    signAction: globals_1.jest.fn(),
    abortAction: globals_1.jest.fn(),
    listActions: globals_1.jest.fn(),
    internalizeAction: globals_1.jest.fn(),
    listOutputs: globals_1.jest.fn(),
    relinquishOutput: globals_1.jest.fn(),
    acquireCertificate: globals_1.jest.fn(),
    listCertificates: globals_1.jest.fn(),
    proveCertificate: globals_1.jest.fn(),
    relinquishCertificate: globals_1.jest.fn(),
    discoverByIdentityKey: globals_1.jest.fn(),
    discoverByAttributes: globals_1.jest.fn(),
    isAuthenticated: globals_1.jest.fn(),
    waitForAuthentication: globals_1.jest.fn(),
    getHeight: globals_1.jest.fn(),
    getHeaderForHeight: globals_1.jest.fn(),
    getNetwork: globals_1.jest.fn(),
    getVersion: globals_1.jest.fn()
};
/**
 * A mock function that simulates building an underlying wallet.
 */
const mockWalletBuilder = globals_1.jest.fn(async (primaryKey, privilegedKeyManager) => {
    // Return our mock underlying wallet object.
    return mockUnderlyingWallet;
});
/**
 * A mock UMPTokenInteractor implementation.
 * We can track whether buildAndSend is called with the right arguments, etc.
 */
const mockUMPTokenInteractor = {
    findByPresentationKeyHash: globals_1.jest.fn(async (hash) => undefined),
    findByRecoveryKeyHash: globals_1.jest.fn(async (hash) => undefined),
    buildAndSend: globals_1.jest.fn(async (wallet, admin, token, oldToken) => 'abcd.0')
};
/**
 * A mock "recoveryKeySaver" that claims it always saved the key successfully.
 */
const mockRecoveryKeySaver = globals_1.jest.fn(async (key) => true);
/**
 * A mock "passwordRetriever" that we can customize to return a specific password
 * or throw if needed.
 */
const mockPasswordRetriever = globals_1.jest.fn(async () => 'test-password');
const XOR = (n1, n2) => {
    if (n1.length !== n2.length) {
        throw new Error('lengths mismatch');
    }
    const r = new Array(n1.length);
    for (let i = 0; i < n1.length; i++) {
        r[i] = n1[i] ^ n2[i];
    }
    return r;
};
// Generate some globals
const presentationKey = (0, sdk_1.Random)(32);
const recoveryKey = (0, sdk_1.Random)(32);
const passwordSalt = (0, sdk_1.Random)(32);
const passwordKey = sdk_1.Hash.pbkdf2(sdk_1.Utils.toArray('test-password', 'utf8'), passwordSalt, CWIStyleWalletManager_1.PBKDF2_NUM_ROUNDS, 32, 'sha512');
const primaryKey = (0, sdk_1.Random)(32);
const privilegedKey = (0, sdk_1.Random)(32);
/**
 * A helper function to create a minimal valid UMP token.
 * This can be used to mock a stored token for existing users.
 */
async function createMockUMPToken() {
    const presentationPassword = new sdk_1.SymmetricKey(XOR(presentationKey, passwordKey));
    const presentationRecovery = new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey));
    const recoveryPassword = new sdk_1.SymmetricKey(XOR(recoveryKey, passwordKey));
    const primaryPassword = new sdk_1.SymmetricKey(XOR(primaryKey, passwordKey));
    const tempPrivilegedKeyManager = new sdk_2.PrivilegedKeyManager(async () => new sdk_1.PrivateKey(privilegedKey));
    return {
        passwordSalt,
        passwordPresentationPrimary: presentationPassword.encrypt(primaryKey),
        passwordRecoveryPrimary: recoveryPassword.encrypt(primaryKey),
        presentationRecoveryPrimary: presentationRecovery.encrypt(primaryKey),
        passwordPrimaryPrivileged: primaryPassword.encrypt(privilegedKey),
        presentationRecoveryPrivileged: presentationRecovery.encrypt(privilegedKey),
        presentationHash: sdk_1.Hash.sha256(presentationKey),
        recoveryHash: sdk_1.Hash.sha256(recoveryKey),
        presentationKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
            plaintext: presentationKey,
            protocolID: [2, 'admin key wrapping'],
            keyID: '1'
        })).ciphertext,
        passwordKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
            plaintext: passwordKey,
            protocolID: [2, 'admin key wrapping'],
            keyID: '1'
        })).ciphertext,
        recoveryKeyEncrypted: (await tempPrivilegedKeyManager.encrypt({
            plaintext: recoveryKey,
            protocolID: [2, 'admin key wrapping'],
            keyID: '1'
        })).ciphertext,
        currentOutpoint: 'abcd:0'
    };
}
describe('CWIStyleWalletManager Tests', () => {
    let manager;
    beforeEach(() => {
        // Reset all mock calls
        globals_1.jest.clearAllMocks();
        // We create a new manager for each test, with no initial snapshot
        manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', // admin originator
        mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever
        // no state snapshot
        );
    });
    // ----------------------------------------------------------------------------------------
    // Private method tests (just to ensure coverage).
    // We'll call them via (manager as any).somePrivateMethod(...) if needed.
    // ----------------------------------------------------------------------------------------
    test('XOR function: verifies correctness', () => {
        const fnXOR = manager.XOR;
        const a = [0x00, 0xff, 0xaa];
        const b = [0xff, 0xff, 0x55];
        const result = fnXOR(a, b);
        // 0x00 ^ 0xFF = 0xFF
        // 0xFF ^ 0xFF = 0x00
        // 0xAA ^ 0x55 = 0xFF
        expect(result).toEqual([0xff, 0x00, 0xff]);
    });
    // ----------------------------------------------------------------------------------------
    // Authentication flows
    // ----------------------------------------------------------------------------------------
    describe('New user flow: presentation + password', () => {
        test('Successfully creates a new token and calls buildAndSend', async () => {
            // New wallet funder is a mock function
            const newWalletFunder = globals_1.jest.fn(() => { });
            manager.newWalletFunder = newWalletFunder;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            // Provide a presentation key
            await manager.providePresentationKey(presentationKey);
            expect(manager.authenticationFlow).toBe('new-user');
            // Provide a password
            mockPasswordRetriever.mockResolvedValueOnce('dummy-password');
            await manager.providePassword('dummy-password');
            // The wallet should now be built, so manager is authenticated
            expect(manager.authenticated).toBe(true);
            // Recovery key saver should have been called
            expect(mockRecoveryKeySaver).toHaveBeenCalledTimes(1);
            // The underlying wallet builder should have been called exactly once
            expect(mockWalletBuilder).toHaveBeenCalledTimes(1);
            // The manager should have called buildAndSend on the interactor
            expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(1);
            const buildArgs = mockUMPTokenInteractor.buildAndSend.mock.calls[0];
            // [0] => the wallet, [1] => adminOriginator, [2] => newToken, [3] => oldToken
            expect(buildArgs[1]).toBe('admin.walletvendor.com');
            expect(buildArgs[2]).toHaveProperty('presentationHash');
            expect(buildArgs[3]).toBeUndefined(); // Because it's a new user (no old token)
            expect(newWalletFunder).toHaveBeenCalled(); // New wallet funder should have been called
        });
        test('Throws if user tries to provide recovery key during new-user flow', async () => {
            // Mark it as new user flow by no token found
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(Array.from({ length: 32 }, () => 1));
            await expect(manager.provideRecoveryKey(Array.from({ length: 32 }, () => 2))).rejects.toThrow('Do not submit recovery key in new-user flow');
        });
    });
    describe('Existing user flow: presentation + password', () => {
        test('Decryption of primary key and building the wallet', async () => {
            // Provide a mock UMP token
            const mockToken = await createMockUMPToken();
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(mockToken);
            // Provide presentation
            await manager.providePresentationKey(presentationKey);
            expect(manager.authenticationFlow).toBe('existing-user');
            // Provide password
            // The manager's internal code will do PBKDF2 with the password + token.passwordSalt
            // Then XOR that with the presentation key for decryption.
            await manager.providePassword('test-password');
            // Check that manager is authenticated
            expect(manager.authenticated).toBe(true);
            // Underlying wallet is built
            expect(mockWalletBuilder).toHaveBeenCalledTimes(1);
        });
    });
    describe('Existing user flow: presentation + recovery key', () => {
        beforeEach(async () => {
            manager.authenticationMode = 'presentation-key-and-recovery-key';
            manager.authenticationFlow = 'existing-user';
        });
        test('Successfully decrypts with presentation+recovery', async () => {
            // Provide a mock UMP token
            const mockToken = await createMockUMPToken();
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(mockToken);
            await manager.providePresentationKey(presentationKey);
            // Provide the recovery key.
            // In "presentation-key-and-recovery-key" mode, the manager won't need the password at all.
            await manager.provideRecoveryKey(recoveryKey);
            expect(manager.authenticated).toBe(true);
            expect(mockWalletBuilder).toHaveBeenCalledTimes(1);
        });
        test('Throws if presentation key not provided first', async () => {
            const recoveryKey = Array.from({ length: 32 }, () => 8);
            await expect(manager.provideRecoveryKey(recoveryKey)).rejects.toThrow('Provide the presentation key first');
        });
    });
    describe('Existing user flow: recovery key + password', () => {
        beforeEach(async () => {
            manager.authenticationMode = 'recovery-key-and-password';
            manager.authenticationFlow = 'existing-user';
        });
        test('Works with correct keys, sets mode as existing-user', async () => {
            const mockToken = await createMockUMPToken();
            mockUMPTokenInteractor.findByRecoveryKeyHash.mockResolvedValueOnce(mockToken);
            // Provide recovery key
            await manager.provideRecoveryKey(recoveryKey);
            // Provide password
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            expect(mockWalletBuilder).toHaveBeenCalledTimes(1);
        });
        test('Throws if no token found by recovery key hash', async () => {
            ;
            mockUMPTokenInteractor.findByRecoveryKeyHash.mockResolvedValueOnce(undefined);
            await expect(manager.provideRecoveryKey(recoveryKey)).rejects.toThrow('No user found with this recovery key');
        });
    });
    // ----------------------------------------------------------------------------------------
    // Snapshots
    // ----------------------------------------------------------------------------------------
    describe('saveSnapshot / loadSnapshot', () => {
        test('Saves a snapshot and can load it into a fresh manager instance', async () => {
            // We'll do a new user flow so that manager is authenticated with a real token.
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            const presKey = Array.from({ length: 32 }, () => 0xa1);
            await manager.providePresentationKey(presKey);
            await manager.providePassword('mypassword'); // triggers creation of new user
            const snapshot = manager.saveSnapshot();
            expect(Array.isArray(snapshot)).toBe(true);
            expect(snapshot.length).toBeGreaterThan(64); // 32 bytes + encrypted data
            // Now create a fresh manager:
            const freshManager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever);
            // Not authenticated yet
            await expect(() => freshManager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated');
            // Load the snapshot
            await freshManager.loadSnapshot(snapshot);
            // The fresh manager is now authenticated (underlying wallet will be built).
            await expect(freshManager.getPublicKey({ identityKey: true })).resolves.not.toThrow();
            // It calls walletBuilder again
            expect(mockWalletBuilder).toHaveBeenCalledTimes(2); // once for the old manager, once for the fresh
        });
        test('Throws error if saving snapshot while no primary key or token set', async () => {
            // Manager is not yet authenticated
            expect(() => manager.saveSnapshot()).toThrow('No root primary key or current UMP token set');
        });
        test('Throws if snapshot is corrupt or cannot be decrypted', async () => {
            // Attempt to load an invalid snapshot
            await expect(() => manager.loadSnapshot([1, 2, 3])).rejects.toThrow('Failed to load snapshot');
        });
    });
    // ----------------------------------------------------------------------------------------
    // Changing Keys
    // ----------------------------------------------------------------------------------------
    describe('Change Password', () => {
        test('Requires authentication and updates the UMP token on-chain', async () => {
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password');
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            await manager.changePassword('new-pass');
            expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(2);
        }, 15000); // Argon2id password derivation takes time
        test('Throws if not authenticated', async () => {
            await expect(manager.changePassword('test-password')).rejects.toThrow('Not authenticated or missing required data.');
        });
    });
    describe('Change Recovery Key', () => {
        test('Prompts to save the new key, updates the token', async () => {
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password');
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            mockUMPTokenInteractor.buildAndSend.mockResolvedValueOnce(makeOutpoint('rcv1', 0));
            await manager.changeRecoveryKey();
            // The user is prompted to store the new key
            expect(mockRecoveryKeySaver).toHaveBeenCalledTimes(2); // once when user created, once after changed
            // The UMP token is updated - with v3 tokens, additional call may occur
            expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalled();
        });
        test('Throws if not authenticated', async () => {
            await expect(manager.changeRecoveryKey()).rejects.toThrow('Not authenticated or missing required data.');
        });
    });
    describe('Change Presentation Key', () => {
        test('Requires authentication, re-publishes the token, old token consumed', async () => {
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, async () => 'test-password');
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            mockUMPTokenInteractor.buildAndSend.mockResolvedValueOnce(makeOutpoint('rcv1', 0));
            const newPresKey = Array.from({ length: 32 }, () => 0xee);
            await manager.changePresentationKey(newPresKey);
            expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(2);
        });
    });
    describe('Profile management', () => {
        test('addProfile adds a new profile and updates the UMP token', async () => {
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            const initialProfiles = manager.listProfiles();
            expect(initialProfiles).toHaveLength(1);
            expect(initialProfiles[0].name).toBe('default');
            const getFactorSpy = globals_1.jest.spyOn(manager, 'getFactor').mockImplementation(async () => (0, sdk_1.Random)(32));
            mockUMPTokenInteractor.buildAndSend.mockClear();
            const newProfileId = await manager.addProfile('Work');
            expect(Array.isArray(newProfileId)).toBe(true);
            expect(newProfileId.length).toBe(16);
            const updatedProfiles = manager.listProfiles();
            expect(updatedProfiles).toHaveLength(2);
            const workProfile = updatedProfiles.find(p => p.name === 'Work');
            expect(workProfile).toBeDefined();
            expect(workProfile.active).toBe(false);
            expect(mockUMPTokenInteractor.buildAndSend).toHaveBeenCalledTimes(1);
            getFactorSpy.mockRestore();
        });
        test('syncUMPToken refreshes UMP token and profiles from overlay when newer token exists', async () => {
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            const originalToken = manager.currentUMPToken;
            const rootPrimaryKey = manager.rootPrimaryKey;
            const extraProfile = {
                name: 'overlay-profile',
                id: (0, sdk_1.Random)(16),
                primaryPad: (0, sdk_1.Random)(32),
                privilegedPad: (0, sdk_1.Random)(32),
                createdAt: Math.floor(Date.now() / 1000)
            };
            const profilesJson = JSON.stringify([extraProfile]);
            const profilesBytes = sdk_1.Utils.toArray(profilesJson, 'utf8');
            const profilesEncrypted = new sdk_1.SymmetricKey(rootPrimaryKey).encrypt(profilesBytes);
            const updatedToken = {
                ...originalToken,
                currentOutpoint: makeOutpoint('overlay-tx', 0),
                profilesEncrypted
            };
            const saveSnapshotSpy = globals_1.jest.spyOn(manager, 'saveSnapshot');
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(updatedToken);
            const result = await manager.syncUMPToken();
            expect(result).toBe(true);
            expect(saveSnapshotSpy).toHaveBeenCalled();
            saveSnapshotSpy.mockRestore();
            const profiles = manager.listProfiles();
            expect(profiles.some(p => p.name === 'overlay-profile')).toBe(true);
        });
    });
    test('Destroy callback clears sensitive data', async () => {
        // authenticate as new user
        ;
        mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
        await manager.providePresentationKey(Array.from({ length: 32 }, () => 12));
        await manager.providePassword('some-pass');
        // manager is authenticated
        expect(manager.authenticated).toBe(true);
        // Destroy
        manager.destroy();
        expect(manager.authenticated).toBe(false);
        // And we can confirm that manager won't allow calls
        await expect(() => manager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated');
    });
    // ----------------------------------------------------------------------------------------
    // Proxies / originator checks
    // ----------------------------------------------------------------------------------------
    describe('Proxy method calls', () => {
        beforeEach(async () => {
            // authenticate
            ;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
        });
        test('Throws if user is not authenticated', async () => {
            // force de-auth
            ;
            manager.authenticated = false;
            await expect(() => manager.getPublicKey({ identityKey: true })).rejects.toThrow('User is not authenticated.');
        });
        test('Throws if originator is adminOriginator', async () => {
            await expect(manager.getPublicKey({ identityKey: true }, 'admin.walletvendor.com')).rejects.toThrow('External applications are not allowed to use the admin originator.');
        });
        test('Passes if user is authenticated and originator is not admin', async () => {
            await manager.getPublicKey({ identityKey: true }, 'example.com');
            expect(mockUnderlyingWallet.getPublicKey).toHaveBeenCalledTimes(1);
        });
        test('All proxied methods call underlying with correct arguments', async () => {
            // We'll do a quick spot-check of a few methods:
            await manager.encrypt({ plaintext: [1, 2, 3], protocolID: [1, 'tests'], keyID: '1' }, 'mydomain.com');
            expect(mockUnderlyingWallet.encrypt).toHaveBeenCalledWith({ plaintext: [1, 2, 3], protocolID: [1, 'tests'], keyID: '1' }, 'mydomain.com');
            // TODO: Test all other proxied methods
        });
        test('isAuthenticated() rejects if originator is admin, resolves otherwise', async () => {
            // If admin tries:
            await expect(manager.isAuthenticated({}, 'admin.walletvendor.com')).rejects.toThrow('External applications are not allowed to use the admin originator.');
            // If normal domain:
            const result = await manager.isAuthenticated({}, 'normal.com');
            expect(result).toEqual({ authenticated: true });
        });
        test('waitForAuthentication() eventually resolves', async () => {
            // Already authenticated from beforeEach. So it should immediately return.
            await manager.waitForAuthentication({}, 'normal.com');
            expect(mockUnderlyingWallet.waitForAuthentication).toHaveBeenCalledTimes(1);
        });
    });
    describe('Additional Tests for Password Retriever Callback, Privileged Key Expiry, and UMP Token Serialization', () => {
        let manager;
        beforeEach(() => {
            globals_1.jest.clearAllMocks();
            manager = new CWIStyleWalletManager_1.CWIStyleWalletManager('admin.walletvendor.com', mockWalletBuilder, mockUMPTokenInteractor, mockRecoveryKeySaver, mockPasswordRetriever);
        });
        test('serializeUMPToken and deserializeUMPToken correctly round-trip a UMP token', async () => {
            const token = await createMockUMPToken();
            // We need a token with a currentOutpoint for serialization.
            expect(token.currentOutpoint).toBeDefined();
            const serializeFn = manager.serializeUMPToken;
            const deserializeFn = manager.deserializeUMPToken;
            const serialized = serializeFn(token);
            expect(Array.isArray(serialized)).toBe(true);
            expect(serialized.length).toBeGreaterThan(0);
            const deserialized = deserializeFn(serialized);
            expect(deserialized).toEqual(token);
        });
        test('Password retriever callback: the test function is passed and supports async validation', async () => {
            let capturedTestFn = null;
            const customPasswordRetriever = globals_1.jest.fn(async (reason, testFn) => {
                capturedTestFn = testFn;
                // In a real scenario the test function would validate a candidate.
                // For our test we simply return the correct password.
                return 'test-password';
            });
            manager.passwordRetriever = customPasswordRetriever;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            // Clear the privileged key so the callback gets ran
            globals_1.jest.advanceTimersByTime(121000);
            // Let's trigger a privileged operation
            await manager.changePassword('test-password'); // trigger some privileged operation...
            expect(customPasswordRetriever).toHaveBeenCalled();
            expect(capturedTestFn).not.toBeNull();
            // Since the internal test function may now be async (Argon2 path), await the result.
            const testResult = await capturedTestFn('any-input');
            expect(typeof testResult).toBe('boolean');
            expect(await capturedTestFn('any-input')).toBe(false);
        }, 15000); // Argon2id password derivation in changePassword takes time
        test('Privileged key expiry: each call to decrypt via the privileged manager invokes passwordRetriever', async () => {
            // In a new-user flow, buildUnderlying is called without a privilegedKey,
            // so any later use of the privileged manager will trigger a password prompt.
            const customPasswordRetriever = globals_1.jest.fn(async (reason, testFn) => {
                return 'test-password';
            });
            manager.passwordRetriever = customPasswordRetriever;
            mockUMPTokenInteractor.findByPresentationKeyHash.mockResolvedValueOnce(undefined);
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            // Clear any calls recorded during authentication.
            customPasswordRetriever.mockClear();
            // Call the underlying privileged key manager’s decrypt twice.
            // (For example, we use the ciphertext from one of the token’s encrypted fields.)
            await manager.rootPrivilegedKeyManager.decrypt({
                ciphertext: manager.currentUMPToken.passwordKeyEncrypted,
                protocolID: [2, 'admin key wrapping'],
                keyID: '1'
            });
            // Key expires after 2 minutes
            globals_1.jest.advanceTimersByTime(121000);
            await manager.rootPrivilegedKeyManager.decrypt({
                ciphertext: manager.currentUMPToken.passwordKeyEncrypted,
                protocolID: [2, 'admin key wrapping'],
                keyID: '1'
            });
            // Since no ephemeral privileged key was provided when building the underlying wallet,
            // each call to decrypt should have resulted in a call to passwordRetriever.
            expect(customPasswordRetriever).toHaveBeenCalledTimes(2);
        });
    });
    describe('UMP v3 KDF Tests', () => {
        let manager;
        let mockInteractor;
        function makeManager(interactor = mockInteractor) {
            return new CWIStyleWalletManager_1.CWIStyleWalletManager('test.admin', mockWalletBuilder, interactor, mockRecoveryKeySaver, mockPasswordRetriever);
        }
        function makeLegacyToken(rootPrimary, outpoint = 'legacy.0') {
            return {
                passwordSalt,
                passwordPresentationPrimary: new sdk_1.SymmetricKey(XOR(presentationKey, passwordKey)).encrypt(rootPrimary),
                passwordRecoveryPrimary: new sdk_1.SymmetricKey(XOR(recoveryKey, passwordKey)).encrypt(rootPrimary),
                presentationRecoveryPrimary: new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey)).encrypt(rootPrimary),
                passwordPrimaryPrivileged: new sdk_1.SymmetricKey(XOR(rootPrimary, passwordKey)).encrypt((0, sdk_1.Random)(32)),
                presentationRecoveryPrivileged: new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey)).encrypt((0, sdk_1.Random)(32)),
                presentationHash: sdk_1.Hash.sha256(presentationKey),
                recoveryHash: sdk_1.Hash.sha256(recoveryKey),
                presentationKeyEncrypted: (0, sdk_1.Random)(48),
                passwordKeyEncrypted: (0, sdk_1.Random)(48),
                recoveryKeyEncrypted: (0, sdk_1.Random)(48),
                currentOutpoint: outpoint
            };
        }
        async function deriveArgon2Key(iterations, memorySize) {
            return Array.from(await (0, hash_wasm_1.argon2id)({
                password: new Uint8Array(sdk_1.Utils.toArray('test-password', 'utf8')),
                salt: new Uint8Array(passwordSalt),
                iterations,
                memorySize,
                parallelism: 1,
                hashLength: 32,
                outputType: 'binary'
            }));
        }
        function makeV3Token(argon2PasswordKey, rootPrimary, outpoint, kdfParams) {
            return {
                passwordSalt,
                passwordPresentationPrimary: new sdk_1.SymmetricKey(XOR(presentationKey, argon2PasswordKey)).encrypt(rootPrimary),
                passwordRecoveryPrimary: new sdk_1.SymmetricKey(XOR(recoveryKey, argon2PasswordKey)).encrypt(rootPrimary),
                presentationRecoveryPrimary: new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey)).encrypt(rootPrimary),
                passwordPrimaryPrivileged: new sdk_1.SymmetricKey(XOR(rootPrimary, argon2PasswordKey)).encrypt((0, sdk_1.Random)(32)),
                presentationRecoveryPrivileged: new sdk_1.SymmetricKey(XOR(presentationKey, recoveryKey)).encrypt((0, sdk_1.Random)(32)),
                presentationHash: sdk_1.Hash.sha256(presentationKey),
                recoveryHash: sdk_1.Hash.sha256(recoveryKey),
                presentationKeyEncrypted: (0, sdk_1.Random)(48),
                passwordKeyEncrypted: (0, sdk_1.Random)(48),
                recoveryKeyEncrypted: (0, sdk_1.Random)(48),
                currentOutpoint: outpoint,
                umpVersion: 3,
                passwordKdf: { algorithm: 'argon2id', parallelism: 1, hashLength: 32, ...kdfParams }
            };
        }
        beforeEach(() => {
            globals_1.jest.clearAllMocks();
            mockInteractor = {
                findByPresentationKeyHash: globals_1.jest.fn(async () => undefined),
                findByRecoveryKeyHash: globals_1.jest.fn(async () => undefined),
                buildAndSend: globals_1.jest.fn(async () => 'txid.0')
            };
        });
        test('Legacy token login still uses PBKDF2 fixed rounds', async () => {
            const legacyToken = makeLegacyToken((0, sdk_1.Random)(32));
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => legacyToken);
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
            // Verify PBKDF2 was used (indirectly via successful auth with legacy token)
        });
        test('V3 token login uses Argon2id and respects iterations', async () => {
            const argon2PasswordKey = await deriveArgon2Key(3, 65536);
            const v3Token = makeV3Token(argon2PasswordKey, (0, sdk_1.Random)(32), 'v3.0', { iterations: 3, memoryKiB: 65536 });
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => v3Token);
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            // Note: This test verifies the KDF config is used during password derivation
            // The actual Argon2id execution happens inside derivePasswordKey()
            await manager.providePassword('test-password');
            expect(manager.authenticated).toBe(true);
        });
        test('Argon2 default params used for new v3 tokens', async () => {
            var _a, _b, _c;
            // Create manager with default Argon2id config (no kdfConfig param)
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            expect(mockInteractor.buildAndSend).toHaveBeenCalled();
            const tokenArg = mockInteractor.buildAndSend.mock.calls[0][2];
            // Verify new token has v3 metadata with Argon2id defaults
            expect(tokenArg.umpVersion).toBe(3);
            expect((_a = tokenArg.passwordKdf) === null || _a === void 0 ? void 0 : _a.algorithm).toBe('argon2id');
            expect((_b = tokenArg.passwordKdf) === null || _b === void 0 ? void 0 : _b.iterations).toBeDefined();
            expect((_c = tokenArg.passwordKdf) === null || _c === void 0 ? void 0 : _c.memoryKiB).toBeDefined();
        });
        test('Round-trip serialization/deserialization for token with KDF metadata', async () => {
            var _a, _b, _c;
            // Use lighter Argon2id params for faster test execution
            const argon2PasswordKey = await deriveArgon2Key(3, 65536);
            const v3Token = makeV3Token(argon2PasswordKey, (0, sdk_1.Random)(32), 'round-trip.0', { iterations: 3, memoryKiB: 65536 });
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => v3Token);
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            const snapshot = manager.saveSnapshot();
            expect(snapshot).toBeTruthy();
            // Create new manager and load snapshot
            const manager2 = new CWIStyleWalletManager_1.CWIStyleWalletManager('test.admin', mockWalletBuilder, mockInteractor, mockRecoveryKeySaver, mockPasswordRetriever, undefined, snapshot);
            // currentUMPToken is set synchronously before loadSnapshot's first await,
            // so no timer wait is needed here
            const loadedToken = manager2.currentUMPToken;
            expect(loadedToken.umpVersion).toBe(3);
            expect((_a = loadedToken.passwordKdf) === null || _a === void 0 ? void 0 : _a.algorithm).toBe('argon2id');
            expect((_b = loadedToken.passwordKdf) === null || _b === void 0 ? void 0 : _b.iterations).toBe(3);
            expect((_c = loadedToken.passwordKdf) === null || _c === void 0 ? void 0 : _c.memoryKiB).toBe(65536);
        }, 30000); // Multiple Argon2id derivations are CPU-intensive
        test('Mixed compatibility: load legacy snapshot then load v3 snapshot', async () => {
            // Load legacy token (uses PBKDF2)
            const legacyToken = makeLegacyToken((0, sdk_1.Random)(32));
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => legacyToken);
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            const legacySnapshot = manager.saveSnapshot();
            // Now create v3 token with Argon2id
            const argon2PasswordKey = await deriveArgon2Key(7, 131072);
            const v3Token = makeV3Token(argon2PasswordKey, (0, sdk_1.Random)(32), 'v3.0', { iterations: 7, memoryKiB: 131072 });
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => v3Token);
            const manager2 = makeManager();
            await manager2.providePresentationKey(presentationKey);
            await manager2.providePassword('test-password');
            const v3Snapshot = manager2.saveSnapshot();
            // Both snapshots should be loadable
            expect(legacySnapshot).toBeTruthy();
            expect(v3Snapshot).toBeTruthy();
            expect(v3Snapshot.length).toBeGreaterThan(legacySnapshot.length); // V3 has extra metadata
        });
        test('Change-password on v3 token preserves Argon2 metadata', async () => {
            var _a, _b, _c;
            const argon2PasswordKey = await deriveArgon2Key(7, 131072);
            const v3Token = makeV3Token(argon2PasswordKey, (0, sdk_1.Random)(32), 'change-pwd.0', { iterations: 7, memoryKiB: 131072 });
            mockInteractor.findByPresentationKeyHash = globals_1.jest.fn(async () => v3Token);
            manager = makeManager();
            await manager.providePresentationKey(presentationKey);
            await manager.providePassword('test-password');
            // Mock getFactor to return mock values (since our test token has random encrypted fields)
            const mockGetFactor = globals_1.jest.spyOn(manager, 'getFactor');
            mockGetFactor.mockImplementation(async (factorName) => {
                if (factorName === 'recoveryKey')
                    return recoveryKey;
                if (factorName === 'presentationKey')
                    return presentationKey;
                if (factorName === 'privilegedKey')
                    return (0, sdk_1.Random)(32);
                return (0, sdk_1.Random)(32);
            });
            await manager.changePassword('new-test-password');
            expect(mockInteractor.buildAndSend).toHaveBeenCalled();
            const updatedToken = mockInteractor.buildAndSend.mock.calls[0][2];
            // Verify KDF metadata preserved
            expect(updatedToken.umpVersion).toBe(3);
            expect((_a = updatedToken.passwordKdf) === null || _a === void 0 ? void 0 : _a.algorithm).toBe('argon2id');
            expect((_b = updatedToken.passwordKdf) === null || _b === void 0 ? void 0 : _b.iterations).toBe(7);
            expect((_c = updatedToken.passwordKdf) === null || _c === void 0 ? void 0 : _c.memoryKiB).toBe(131072);
        });
    });
    describe('OverlayUMPTokenInteractor signature-aware parsing', () => {
        test('strips verified trailing signature before interpreting optional profiles', () => {
            const interactor = new CWIStyleWalletManager_1.OverlayUMPTokenInteractor({}, {});
            const payloadFields = Array.from({ length: 11 }, () => (0, sdk_1.Random)(32));
            const signingKey = sdk_1.PrivateKey.fromRandom();
            const validSignature = signingKey.sign(payloadFields.flat()).toDER();
            const fromBeefSpy = globals_1.jest.spyOn(sdk_1.Transaction, 'fromBEEF').mockReturnValue({
                outputs: [{ lockingScript: {} }],
                id: () => 'txid123'
            });
            const decodeSpy = globals_1.jest.spyOn(sdk_1.PushDrop, 'decode').mockReturnValue({
                fields: [...payloadFields, validSignature],
                lockingPublicKey: signingKey.toPublicKey()
            });
            const parsed = interactor.parseLookupAnswer({
                type: 'output-list',
                outputs: [{ beef: [1, 2, 3], outputIndex: 0 }]
            });
            expect(parsed).toBeDefined();
            expect(parsed.passwordSalt).toEqual(payloadFields[0]);
            expect(parsed.profilesEncrypted).toBeUndefined();
            fromBeefSpy.mockRestore();
            decodeSpy.mockRestore();
        });
        test('does not strip DER-like trailing field when signature verification fails', () => {
            const interactor = new CWIStyleWalletManager_1.OverlayUMPTokenInteractor({}, {});
            const payloadFields = Array.from({ length: 11 }, () => (0, sdk_1.Random)(32));
            const derLikeButInvalidForPayload = [0x30, 0x06, 1, 2, 3, 4, 5, 6];
            const fromBeefSpy = globals_1.jest.spyOn(sdk_1.Transaction, 'fromBEEF').mockReturnValue({
                outputs: [{ lockingScript: {} }],
                id: () => 'txid124'
            });
            const decodeSpy = globals_1.jest.spyOn(sdk_1.PushDrop, 'decode').mockReturnValue({
                fields: [...payloadFields, derLikeButInvalidForPayload],
                lockingPublicKey: sdk_1.PrivateKey.fromRandom().toPublicKey()
            });
            const parsed = interactor.parseLookupAnswer({
                type: 'output-list',
                outputs: [{ beef: [4, 5, 6], outputIndex: 0 }]
            });
            expect(parsed).toBeDefined();
            expect(parsed.profilesEncrypted).toEqual(derLikeButInvalidForPayload);
            fromBeefSpy.mockRestore();
            decodeSpy.mockRestore();
        });
    });
});
//# sourceMappingURL=CWIStyleWalletManager.test.js.map