import * as cbor from '@ipld/dag-cbor';
import { CID } from 'multiformats/cid';
import { DataStream } from '../../src/utils/data-stream.js';
import { DidKey } from '@enbox/dids';
import { ed25519 } from '../../src/jose/algorithms/signing/ed25519.js';
import { Encoder } from '../../src/utils/encoder.js';
import { Jws } from '../../src/utils/jws.js';
import { KeyDerivationScheme } from '../../src/utils/hd-key.js';
import { MessagesQuery } from '../../src/interfaces/messages-query.js';
import { MessagesRead } from '../../src/interfaces/messages-read.js';
import { MessagesSubscribe } from '../../src/interfaces/messages-subscribe.js';
import { PermissionsProtocol } from '../../src/protocols/permissions.js';
import { PrivateKeySigner } from '../../src/utils/private-key-signer.js';
import { ProtocolsConfigure } from '../../src/interfaces/protocols-configure.js';
import { ProtocolsQuery } from '../../src/interfaces/protocols-query.js';
import { RecordsCount } from '../../src/interfaces/records-count.js';
import { RecordsDelete } from '../../src/interfaces/records-delete.js';
import { RecordsQuery } from '../../src/interfaces/records-query.js';
import { RecordsSubscribe } from '../../src/interfaces/records-subscribe.js';
import { RecordsWrite } from '../../src/interfaces/records-write.js';
import { removeUndefinedProperties } from '@enbox/common';
import { Secp256k1 } from '../../src/utils/secp256k1.js';
import { sha256 } from 'multiformats/hashes/sha2';
import { Time } from '../../src/utils/time.js';
import { X25519 } from '@enbox/crypto';
import { ContentEncryptionAlgorithm, Encryption } from '../../src/utils/encryption.js';
import { DwnInterfaceName, DwnMethodName } from '../../src/enums/dwn-interface-method.js';
/**
 * Protocol definition that matches the defaults used by `TestDataGenerator.generateRecordsWrite()`:
 * `protocol: 'http://test-protocol.xyz'`, `protocolPath: 'testRecord'`.
 *
 * Install via `TestDataGenerator.installDefaultTestProtocol(dwn, persona)`.
 */
export const defaultTestProtocolDefinition = {
    protocol: 'http://test-protocol.xyz',
    published: false,
    types: {
        testRecord: {}
    },
    structure: {
        testRecord: {}
    }
};
/**
 * Utility class for generating data for testing.
 */
export class TestDataGenerator {
    /**
     * Generates a persona.
     */
    static async generatePersona(input) {
        // generate DID if not given
        let did = input?.did;
        if (!did) {
            const didSuffix = TestDataGenerator.randomString(32);
            did = `did:example:${didSuffix}`;
        }
        // generate persona key ID if not given
        const keyIdSuffix = TestDataGenerator.randomString(10);
        const keyId = input?.keyId ?? `${did}#${keyIdSuffix}`;
        // generate persona signing key pair if not given (secp256k1 for ES256K signatures)
        const keyPair = input?.keyPair ?? await Secp256k1.generateKeyPair();
        // generate persona encryption key pair if not given (X25519 for DWN encryption)
        let encryptionKeyPair = input?.encryptionKeyPair;
        if (!encryptionKeyPair) {
            const encPrivateKey = await X25519.generateKey();
            const encPublicKey = await X25519.getPublicKey({ key: encPrivateKey });
            encryptionKeyPair = {
                publicJwk: encPublicKey,
                privateJwk: encPrivateKey,
            };
        }
        const persona = {
            did,
            keyId,
            keyPair,
            encryptionKeyPair,
            signer: new PrivateKeySigner({
                privateJwk: keyPair.privateJwk,
                algorithm: keyPair.privateJwk.alg,
                keyId: `${did}#${keyId}`,
            })
        };
        return persona;
    }
    /**
     * Installs the default test protocol (`http://test-protocol.xyz` with `testRecord` type)
     * on the given DWN for the given persona. Call this before processing any RecordsWrite
     * created with `generateRecordsWrite()` that uses the default protocol.
     */
    static async installDefaultTestProtocol(dwn, persona) {
        const protocolsConfigure = await ProtocolsConfigure.create({
            definition: defaultTestProtocolDefinition,
            signer: Jws.createSigner(persona),
        });
        const reply = await dwn.processMessage(persona.did, protocolsConfigure.message);
        if (reply.status.code !== 202) {
            throw new Error(`Failed to install default test protocol: ${reply.status.code} ${reply.status.detail}`);
        }
    }
    /**
     * Generates a ProtocolsConfigure message for testing.
     * Optional parameters are generated if not given.
     * Implementation currently uses `ProtocolsConfigure.create()`.
     */
    static async generateProtocolsConfigure(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        // generate protocol types and  definition if not given
        let definition = input?.protocolDefinition;
        if (!definition) {
            const generatedLabel = 'record' + TestDataGenerator.randomString(10);
            definition = {
                protocol: TestDataGenerator.randomString(20),
                published: input?.published ?? false,
                types: {},
                structure: {}
            };
            definition.types[generatedLabel] = {
                schema: `test-object`,
                dataFormats: ['text/plain']
            };
            definition.structure[generatedLabel] = {};
        }
        const signer = Jws.createSigner(author);
        const options = {
            messageTimestamp: input?.messageTimestamp,
            definition,
            signer,
            permissionGrantId: input?.permissionGrantId,
            delegatedGrant: input?.delegatedGrant
        };
        const protocolsConfigure = await ProtocolsConfigure.create(options);
        return {
            author,
            message: protocolsConfigure.message,
            protocolsConfigure
        };
    }
    ;
    /**
     * Generates a ProtocolsQuery message for testing.
     */
    static async generateProtocolsQuery(input) {
        // generate author persona if not given
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const signer = Jws.createSigner(author);
        const options = {
            messageTimestamp: input?.messageTimestamp,
            filter: input?.filter,
            signer,
            permissionGrantId: input?.permissionGrantId,
        };
        removeUndefinedProperties(options);
        const protocolsQuery = await ProtocolsQuery.create(options);
        return {
            author,
            message: protocolsQuery.message,
            protocolsQuery
        };
    }
    ;
    static async generateGrantCreate(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const grantedToPersona = input?.grantedTo ?? await TestDataGenerator.generatePersona();
        const dateExpires = input?.dateExpires ?? Time.createOffsetTimestamp({ seconds: 10 });
        const scope = input?.scope ?? {
            interface: DwnInterfaceName.Messages,
            method: DwnMethodName.Read
        };
        const signer = Jws.createSigner(author);
        const grantedTo = grantedToPersona.did;
        const options = {
            signer,
            grantedTo,
            dateExpires,
            scope,
            description: input?.description ?? TestDataGenerator.randomString(10),
            delegated: input?.delegated ?? false,
            requestId: input?.requestId,
            conditions: input?.conditions,
        };
        const grant = await PermissionsProtocol.createGrant(options);
        const dataStream = DataStream.fromBytes(grant.permissionGrantBytes);
        return {
            dataStream,
            recordsWrite: grant.recordsWrite,
            dataBytes: grant.permissionGrantBytes,
            message: grant.recordsWrite.message,
            dataEncodedMessage: grant.dataEncodedMessage
        };
    }
    ;
    /**
     * Generates a RecordsWrite message for testing.
     * `dataBytes` & `dataStream` returned will be `undefined` as long as `dataCid` or `dataSize` is given.
     * Implementation currently uses `RecordsWrite.create()`.
     * @param input.attesters Attesters of the message. Will NOT be generated if not given.
     * @param input.data Data that belongs to the record. Generated when not given only if `dataCid` and `dataSize` are also not given.
     * @param input.dataFormat Format of the data. Defaults to 'application/json' if not given.
     * @param input.signer MessageSigner of the message. Generated if not given.
     * @param input.schema Schema of the message. Randomly generated if not given.
     */
    static async generateRecordsWrite(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const signer = Jws.createSigner(author);
        const attestationSigners = Jws.createSigners(input?.attesters ?? []);
        const dataCid = input?.dataCid;
        const dataSize = input?.dataSize;
        let dataBytes;
        let dataStream;
        if (dataCid === undefined && dataSize === undefined) {
            dataBytes = input?.data ?? TestDataGenerator.randomBytes(32);
            dataStream = DataStream.fromBytes(dataBytes);
        }
        const options = {
            recipient: input?.recipient,
            protocol: input?.protocol ?? 'http://test-protocol.xyz',
            protocolPath: input?.protocolPath ?? 'testRecord',
            protocolRole: input?.protocolRole,
            schema: input?.schema ?? `http://${TestDataGenerator.randomString(20)}`,
            tags: input?.tags,
            recordId: input?.recordId,
            parentContextId: input?.parentContextId,
            published: input?.published,
            dataFormat: input?.dataFormat ?? 'application/json',
            dateCreated: input?.dateCreated,
            messageTimestamp: input?.messageTimestamp,
            datePublished: input?.datePublished,
            data: dataBytes,
            dataCid,
            dataSize,
            signer,
            attestationSigners,
            encryptionInput: input?.encryptionInput,
            permissionGrantId: input?.permissionGrantId,
            squash: input?.squash,
        };
        const recordsWrite = await RecordsWrite.create(options);
        const message = recordsWrite.message;
        return {
            author,
            message,
            dataCid,
            dataSize,
            dataBytes,
            dataStream,
            recordsWrite
        };
    }
    ;
    /**
     * Generates a encrypted RecordsWrite message for testing.
     *
     * @param input.protocolDefinition Protocol definition used to generate the RecordsWrite.
     *        Must be the RECIPIENT's protocol definition if `encryptSymmetricKeyWithProtocolPathDerivedKey` is true,
     *        because the recipient's public keys will be needed to encrypt the symmetric key.
     *
     * @param input.encryptSymmetricKeyWithProtocolPathDerivedKey
     *        Set to `true` to attach the symmetric key encrypted by the protocol path derived public key
     *
     */
    static async generateProtocolEncryptedRecordsWrite(input) {
        const { plaintextBytes, author, recipient, protocolDefinition, protocolPath, protocolParentContextId, } = input;
        // encrypt the plaintext data for the target with a randomly generated symmetric key
        const dataEncryptionInitializationVector = TestDataGenerator.randomBytes(16);
        const dataEncryptionKey = TestDataGenerator.randomBytes(32);
        const encryptedDataBytes = await Encryption.encrypt(ContentEncryptionAlgorithm.A256CTR, dataEncryptionKey, dataEncryptionInitializationVector, plaintextBytes);
        // author generates a RecordsWrite using the encrypted data
        const protocolPathSegments = protocolPath.split('/');
        const recordType = protocolPathSegments[protocolPathSegments.length - 1];
        const { message, dataStream, recordsWrite } = await TestDataGenerator.generateRecordsWrite({
            author,
            recipient,
            protocol: protocolDefinition.protocol,
            protocolPath,
            parentContextId: protocolParentContextId,
            schema: protocolDefinition.types[recordType].schema,
            dataFormat: protocolDefinition.types[recordType].dataFormats?.[0],
            data: encryptedDataBytes
        });
        // final encryption input (`keyEncryptionInputs` to be populated below)
        const encryptionInput = {
            initializationVector: dataEncryptionInitializationVector,
            key: dataEncryptionKey,
            keyEncryptionInputs: []
        };
        if (input.encryptSymmetricKeyWithProtocolPathDerivedKey) {
            // locate the rule set corresponding the protocol path of the message
            let protocolRuleSetSegment = protocolDefinition.structure;
            for (const pathSegment of protocolPathSegments) {
                protocolRuleSetSegment = protocolRuleSetSegment[pathSegment];
            }
            const protocolPathDerivedPublicKeyJwk = protocolRuleSetSegment.$keyAgreement?.publicKeyJwk;
            const protocolPathDerivedKeyEncryptionInput = {
                keyId: await Encryption.getKeyId(protocolPathDerivedPublicKeyJwk),
                publicKey: protocolPathDerivedPublicKeyJwk,
                derivationScheme: KeyDerivationScheme.ProtocolPath
            };
            encryptionInput.keyEncryptionInputs.push(protocolPathDerivedKeyEncryptionInput);
        }
        await recordsWrite.encryptSymmetricEncryptionKey(encryptionInput);
        await recordsWrite.sign({ signer: Jws.createSigner(author) });
        return { message, dataStream: dataStream, recordsWrite, encryptedDataBytes, encryptionInput };
    }
    /**
     * Generates a valid RecordsWrite that modifies the given an existing write.
     * Any mutable property is not specified will be automatically mutated.
     * e.g. if `published` is not specified, it will be toggled from the state of the given existing write.
     */
    static async generateFromRecordsWrite(input) {
        const existingMessage = input.existingWrite.message;
        const currentTime = Time.getCurrentTimestamp();
        const published = input.published ?? existingMessage.descriptor.published ? false : true; // toggle from the parent value if not given explicitly
        const datePublished = input.datePublished ?? (published ? currentTime : undefined);
        const dataBytes = input.data ?? TestDataGenerator.randomBytes(32);
        const dataStream = DataStream.fromBytes(dataBytes);
        const options = {
            recordsWriteMessage: input.existingWrite.message,
            data: dataBytes,
            published,
            datePublished,
            messageTimestamp: input.messageTimestamp,
            protocolRole: input.protocolRole,
            tags: input.tags,
            signer: Jws.createSigner(input.author)
        };
        const recordsWrite = await RecordsWrite.createFrom(options);
        return {
            message: recordsWrite.message,
            recordsWrite,
            dataBytes,
            dataStream
        };
    }
    /**
     * Generates a RecordsQuery message for testing.
     */
    static async generateRecordsQuery(input) {
        let author = input?.author;
        const anonymous = input?.anonymous ?? false;
        if (anonymous && author) {
            throw new Error('Cannot have `author` and be anonymous at the same time.');
        }
        // generate author if needed
        if (author === undefined && !anonymous) {
            author = await TestDataGenerator.generatePersona();
        }
        let signer = undefined;
        if (author !== undefined) {
            signer = Jws.createSigner(author);
        }
        const options = {
            messageTimestamp: input?.messageTimestamp,
            signer,
            filter: input?.filter ?? { schema: TestDataGenerator.randomString(10) }, // must have one filter property if no filter is given
            dateSort: input?.dateSort,
            pagination: input?.pagination,
            permissionGrantId: input?.permissionGrantId,
            protocolRole: input?.protocolRole,
        };
        removeUndefinedProperties(options);
        const recordsQuery = await RecordsQuery.create(options);
        const message = recordsQuery.message;
        return {
            author,
            message
        };
    }
    ;
    /**
     * Generates a RecordsSubscribe message for testing.
     */
    static async generateRecordsSubscribe(input) {
        let author = input?.author;
        const anonymous = input?.anonymous ?? false;
        if (anonymous && author) {
            throw new Error('Cannot have `author` and be anonymous at the same time.');
        }
        // generate author if needed
        if (author === undefined && !anonymous) {
            author = await TestDataGenerator.generatePersona();
        }
        let signer = undefined;
        if (author !== undefined) {
            signer = Jws.createSigner(author);
        }
        const options = {
            messageTimestamp: input?.messageTimestamp,
            signer,
            filter: input?.filter ?? { schema: TestDataGenerator.randomString(10) }, // must have one filter property if no filter is given
            dateSort: input?.dateSort,
            pagination: input?.pagination,
            permissionGrantId: input?.permissionGrantId,
            protocolRole: input?.protocolRole,
            cursor: input?.cursor,
        };
        removeUndefinedProperties(options);
        const recordsSubscribe = await RecordsSubscribe.create(options);
        const message = recordsSubscribe.message;
        return {
            author,
            message
        };
    }
    /**
     * Generates a RecordsCount message for testing.
     */
    static async generateRecordsCount(input) {
        let author = input?.author;
        const anonymous = input?.anonymous ?? false;
        if (anonymous && author) {
            throw new Error('Cannot have `author` and be anonymous at the same time.');
        }
        // generate author if needed
        if (author === undefined && !anonymous) {
            author = await TestDataGenerator.generatePersona();
        }
        let signer = undefined;
        if (author !== undefined) {
            signer = Jws.createSigner(author);
        }
        const options = {
            messageTimestamp: input?.messageTimestamp,
            signer,
            filter: input?.filter ?? { schema: TestDataGenerator.randomString(10) },
            permissionGrantId: input?.permissionGrantId,
            protocolRole: input?.protocolRole,
        };
        removeUndefinedProperties(options);
        const recordsCount = await RecordsCount.create(options);
        const message = recordsCount.message;
        return {
            author,
            message
        };
    }
    /**
     * Generates a RecordsDelete for testing.
     */
    static async generateRecordsDelete(input) {
        const author = input?.author ?? await TestDataGenerator.generateDidKeyPersona();
        const recordsDelete = await RecordsDelete.create({
            recordId: input?.recordId ?? await TestDataGenerator.randomCborSha256Cid(),
            protocolRole: input?.protocolRole,
            signer: Jws.createSigner(author)
        });
        return {
            author,
            recordsDelete,
            message: recordsDelete.message
        };
    }
    /**
     * Generates a MessagesSubscribe message for testing.
     */
    static async generateMessagesSubscribe(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const signer = Jws.createSigner(author);
        const options = {
            filters: input?.filters,
            messageTimestamp: input?.messageTimestamp,
            permissionGrantIds: input?.permissionGrantIds,
            cursor: input?.cursor,
            signer,
        };
        removeUndefinedProperties(options);
        const messagesSubscribe = await MessagesSubscribe.create(options);
        const message = messagesSubscribe.message;
        return {
            author,
            messagesSubscribe,
            message
        };
    }
    /**
     * Generates a MessagesQuery message for testing.
     */
    static async generateMessagesQuery(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const signer = Jws.createSigner(author);
        const options = {
            filters: input?.filters,
            messageTimestamp: input?.messageTimestamp,
            permissionGrantIds: input?.permissionGrantIds,
            cursor: input?.cursor,
            limit: input?.limit,
            cidsOnly: input?.cidsOnly,
            signer,
        };
        removeUndefinedProperties(options);
        const messagesQuery = await MessagesQuery.create(options);
        const message = messagesQuery.message;
        return {
            author,
            messagesQuery,
            message
        };
    }
    static async generateMessagesRead(input) {
        const author = input?.author ?? await TestDataGenerator.generatePersona();
        const signer = Jws.createSigner(author);
        const options = {
            signer,
            messageCid: input.messageCid,
            permissionGrantIds: input.permissionGrantIds
        };
        const messagesRead = await MessagesRead.create(options);
        return {
            author,
            messagesRead,
            message: messagesRead.message,
        };
    }
    /**
     * Generates a dummy `authorization` property for a DWN message that only conforms to schema validation.
     */
    static generateAuthorization() {
        return {
            signature: TestDataGenerator.generateAuthorizationSignature()
        };
    }
    /**
     * Generates a dummy `authorization` property for a DWN message that only conforms to schema validation.
     */
    static generateAuthorizationSignature() {
        return {
            payload: 'anyPayload',
            signatures: [{
                    protected: 'anyProtectedHeader',
                    signature: 'anySignature'
                }]
        };
    }
    /**
     * Generates a random but well-formed signature string in Base64Url format.
     */
    static async randomSignatureString() {
        const keyPair = await ed25519.generateKeyPair();
        const signatureBytes = await ed25519.sign(TestDataGenerator.randomBytes(32), keyPair.privateJwk);
        const signatureString = Encoder.bytesToBase64Url(signatureBytes);
        return signatureString;
    }
    /**
     * Generates a random alpha-numeric string.
     */
    static randomString(length) {
        const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
        // pick characters randomly
        let randomString = '';
        for (let i = 0; i < length; i++) {
            randomString += charset.charAt(Math.floor(Math.random() * charset.length));
        }
        return randomString;
    }
    ;
    /**
     * Generates a random byte array of given length.
     */
    static randomBytes(length) {
        const randomBytes = new Uint8Array(length);
        for (let i = 0; i < length; i++) {
            randomBytes[i] = Math.floor(Math.random() * 256);
        }
        return randomBytes;
    }
    ;
    /**
     * Generates a random CBOR SHA256 CID.
     */
    static async randomCborSha256Cid() {
        const randomBytes = TestDataGenerator.randomBytes(32);
        const randomMultihash = await sha256.digest(randomBytes);
        const cid = await CID.createV1(cbor.code, randomMultihash);
        return cid.toString();
    }
    /**
     * Generates a random within a range (inclusive).
     * @param min lowest potential value.
     * @param max greatest potential value.
     */
    static randomInt(min, max) {
        return Math.floor(Math.random() * (max - min) + min);
    }
    /**
     * Generates a random timestamp. Optionally allows you to set specific non-randomized values for the timestamp.
     *
     * @returns random UTC ISO-8601 timestamp
     */
    static randomTimestamp(options) {
        const { year, month, day, hour, minute, second, millisecond, microsecond } = options || {};
        return Time.createTimestamp({
            year: year || this.randomInt(2000, 2022),
            month: month || this.randomInt(1, 12),
            day: day || this.randomInt(1, 28),
            hour: hour || this.randomInt(0, 23),
            minute: minute || this.randomInt(0, 59),
            second: second || this.randomInt(0, 59),
            millisecond: millisecond || this.randomInt(0, 1000),
            microsecond: microsecond || this.randomInt(0, 1000)
        });
    }
    /**
     * Creates a mock DID resolution result for testing purposes.
     */
    static createDidResolutionResult(persona) {
        return {
            didResolutionMetadata: {},
            didDocument: {
                id: persona.did,
                verificationMethod: [{
                        controller: persona.did,
                        id: persona.keyId,
                        type: 'JsonWebKey2020',
                        // TODO: #672 - port and use type from @enbox/crypto - https://github.com/enboxorg/enbox/issues/672
                        publicKeyJwk: persona.keyPair.publicJwk
                    }]
            },
            didDocumentMetadata: {}
        };
    }
    /**
     * Generates a did:key persona.
     */
    static async generateDidKeyPersona() {
        const did = await DidKey.create();
        const signingMethod = await DidKey.getSigningMethod({ didDocument: did.document });
        const keyId = signingMethod.id;
        const portableDid = await did.export();
        const keyPair = {
            // TODO: #672 - port and use type from @enbox/crypto - https://github.com/enboxorg/enbox/issues/672
            publicJwk: signingMethod.publicKeyJwk,
            privateJwk: portableDid.privateKeys[0],
        };
        // Generate X25519 encryption key pair for did:key personas
        const encPrivateKey = await X25519.generateKey();
        const encPublicKey = await X25519.getPublicKey({ key: encPrivateKey });
        const encryptionKeyPair = {
            publicJwk: encPublicKey,
            privateJwk: encPrivateKey,
        };
        return {
            did: did.uri,
            keyId,
            keyPair,
            encryptionKeyPair,
            signer: new PrivateKeySigner({
                privateJwk: keyPair.privateJwk,
                algorithm: keyPair.privateJwk.alg,
                keyId
            })
        };
    }
}
//# sourceMappingURL=test-data-generator.js.map