/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */


import {iot, mqtt as mqtt311, mqtt5, mqtt_request_response} from "aws-crt";
import {once} from "events";
import {v4 as uuid} from "uuid";
import {
    CertificateStatus,
    DeleteCertificateCommand,
    DeleteThingCommand,
    UpdateCertificateCommand,
    IoTClient
} from "@aws-sdk/client-iot";

import {IotIdentityClientv2} from "./iotidentityclientv2";
import fs from "fs";

jest.setTimeout(10000);

function hasTestEnvironment() : boolean {
    if (process.env.AWS_TEST_IOT_CORE_PROVISIONING_HOST === undefined) {
        return false;
    }

    if (process.env.AWS_TEST_IOT_CORE_PROVISIONING_CERTIFICATE_PATH === undefined) {
        return false;
    }

    if (process.env.AWS_TEST_IOT_CORE_PROVISIONING_KEY_PATH === undefined) {
        return false;
    }

    if (process.env.AWS_TEST_IOT_CORE_PROVISIONING_TEMPLATE_NAME === undefined) {
        return false;
    }

    if (process.env.AWS_TEST_IOT_CORE_PROVISIONING_CSR_PATH === undefined) {
        return false;
    }

    return true;
}

const conditional_test = (condition : boolean) => condition ? it : it.skip;

function build_protocol_client_mqtt5() : mqtt5.Mqtt5Client {
    let builder = iot.AwsIotMqtt5ClientConfigBuilder.newDirectMqttBuilderWithMtlsFromPath(
        // @ts-ignore
        process.env.AWS_TEST_IOT_CORE_PROVISIONING_HOST,
        process.env.AWS_TEST_IOT_CORE_PROVISIONING_CERTIFICATE_PATH,
        process.env.AWS_TEST_IOT_CORE_PROVISIONING_KEY_PATH
    );

    builder.withConnectProperties({
        clientId : `test-${uuid()}`,
        keepAliveIntervalSeconds: 1200,
    });

    return new mqtt5.Mqtt5Client(builder.build());
}

function build_protocol_client_mqtt311() : mqtt311.MqttClientConnection {
    // @ts-ignore
    let builder = iot.AwsIotMqttConnectionConfigBuilder.new_mtls_builder_from_path(process.env.AWS_TEST_IOT_CORE_PROVISIONING_CERTIFICATE_PATH, process.env.AWS_TEST_IOT_CORE_PROVISIONING_KEY_PATH);
    // @ts-ignore
    builder.with_endpoint(process.env.AWS_TEST_IOT_CORE_PROVISIONING_HOST);
    builder.with_client_id(`test-${uuid()}`);

    let client = new mqtt311.MqttClient();
    return client.new_connection(builder.build());
}

enum ProtocolVersion {
    Mqtt311,
    Mqtt5
}

interface TestingOptions {
    version: ProtocolVersion,
    timeoutSeconds?: number,
}

class IdentityTestingContext {

    mqtt311Client?: mqtt311.MqttClientConnection;
    mqtt5Client?: mqtt5.Mqtt5Client;

    client: IotIdentityClientv2;

    private protocolStarted : boolean = false;

    async startProtocolClient() {
        if (!this.protocolStarted) {
            this.protocolStarted = true;
            if (this.mqtt5Client) {
                let connected = once(this.mqtt5Client, mqtt5.Mqtt5Client.CONNECTION_SUCCESS);
                this.mqtt5Client.start();

                await connected;
            }

            if (this.mqtt311Client) {
                await this.mqtt311Client.connect();
            }
        }
    }

    async stopProtocolClient() {
        if (this.protocolStarted) {
            this.protocolStarted = false;
            if (this.mqtt5Client) {
                let stopped = once(this.mqtt5Client, mqtt5.Mqtt5Client.STOPPED);
                this.mqtt5Client.stop();
                await stopped;

                this.mqtt5Client.close();
            }

            if (this.mqtt311Client) {
                await this.mqtt311Client.disconnect();
            }
        }
    }

    constructor(options: TestingOptions) {
        if (options.version == ProtocolVersion.Mqtt5) {
            this.mqtt5Client = build_protocol_client_mqtt5();

            let rrOptions : mqtt_request_response.RequestResponseClientOptions = {
                maxRequestResponseSubscriptions : 6,
                maxStreamingSubscriptions : 2,
                operationTimeoutInSeconds : options.timeoutSeconds ?? 60,
            }

            this.client = IotIdentityClientv2.newFromMqtt5(this.mqtt5Client, rrOptions);
        } else {
            this.mqtt311Client = build_protocol_client_mqtt311();

            let rrOptions : mqtt_request_response.RequestResponseClientOptions = {
                maxRequestResponseSubscriptions : 6,
                maxStreamingSubscriptions : 2,
                operationTimeoutInSeconds : options.timeoutSeconds ?? 60,
            }

            this.client = IotIdentityClientv2.newFromMqtt311(this.mqtt311Client, rrOptions);
        }
    }

    async open() {
        await this.startProtocolClient();
    }

    async close() {
        this.client.close();
        await this.stopProtocolClient();
    }
}

async function doCreateDestroyTest(version: ProtocolVersion) {
    let context = new IdentityTestingContext({
        version: version
    });
    await context.open();

    await context.close();
}

conditional_test(hasTestEnvironment())('identityv2 - create destroy mqtt5', async () => {
    await doCreateDestroyTest(ProtocolVersion.Mqtt5);
});

conditional_test(hasTestEnvironment())('identityv2 - create destroy mqtt311', async () => {
    await doCreateDestroyTest(ProtocolVersion.Mqtt311);
});

interface TestResources {
    certificateId?: string,
    thingName?: string
}

//@ts-ignore
let identityResources : TestResources = {};

beforeEach(async () => {
    identityResources = {}
});

afterEach(async () => {
    const client = new IoTClient({});

    if (identityResources.thingName) {
        await new Promise(r => setTimeout(r, 1000));

        const command = new DeleteThingCommand({
            thingName: identityResources.thingName
        });

        try {
            await client.send(command);
        } catch (e) {}
    }

    if (identityResources.certificateId) {
        await new Promise(r => setTimeout(r, 1000));

        const deactivateCommand = new UpdateCertificateCommand({
            certificateId: identityResources.certificateId,
            newStatus: CertificateStatus.INACTIVE
        });

        try {
            await client.send(deactivateCommand);
        } catch (e) {}

        await new Promise(r => setTimeout(r, 1000));

        const deleteCommand = new DeleteCertificateCommand({
            certificateId: identityResources.certificateId,
        });

        try {
            await client.send(deleteCommand);
        } catch (e) {}
    }

    identityResources = {}
});

async function doProvisioningTest(version: ProtocolVersion) {

    let context = new IdentityTestingContext({
        version: version
    });
    await context.open();

    let createKeysResponse = await context.client.createKeysAndCertificate({});
    identityResources.certificateId = createKeysResponse.certificateId;
    expect(createKeysResponse.certificateId).toBeDefined();
    expect(createKeysResponse.certificatePem).toBeDefined();
    expect(createKeysResponse.privateKey).toBeDefined();
    expect(createKeysResponse.certificateOwnershipToken).toBeDefined();

    const params: { [key: string]: string } = JSON.parse(`{"SerialNumber":"${uuid()}"}`);

    let registerThingResponse = await context.client.registerThing({
        // @ts-ignore
        templateName: process.env.AWS_TEST_IOT_CORE_PROVISIONING_TEMPLATE_NAME,
        certificateOwnershipToken: createKeysResponse.certificateOwnershipToken,
        parameters: params
    });
    identityResources.thingName = registerThingResponse.thingName;
    expect(registerThingResponse.thingName).toBeDefined();

    context.client.close();

    await context.close();
}

conditional_test(hasTestEnvironment())('identityv2 provisioning mqtt5', async () => {
    await doProvisioningTest(ProtocolVersion.Mqtt5);
});


conditional_test(hasTestEnvironment())('identityv2 provisioning mqtt311', async () => {
    await doProvisioningTest(ProtocolVersion.Mqtt311);
});

async function doCsrProvisioningTest(version: ProtocolVersion) {
    let csr: string = "";
    try {
        csr = fs.readFileSync(process.env.AWS_TEST_IOT_CORE_PROVISIONING_CSR_PATH as string, 'utf8');
    } catch (e) {
        if (e instanceof Error) {
            console.log('Error reading CSR PEM file:', e.stack);
        }
    }

    let context = new IdentityTestingContext({
        version: version
    });
    await context.open();

    let createCertificateFromCsrResponse = await context.client.createCertificateFromCsr({
        certificateSigningRequest: csr
    });
    identityResources.certificateId = createCertificateFromCsrResponse.certificateId;
    expect(createCertificateFromCsrResponse.certificateId).toBeDefined();
    expect(createCertificateFromCsrResponse.certificatePem).toBeDefined();
    expect(createCertificateFromCsrResponse.certificateOwnershipToken).toBeDefined();

    const params: { [key: string]: string } = JSON.parse(`{"SerialNumber":"${uuid()}"}`);

    let registerThingResponse = await context.client.registerThing({
        // @ts-ignore
        templateName: process.env.AWS_TEST_IOT_CORE_PROVISIONING_TEMPLATE_NAME,
        certificateOwnershipToken: createCertificateFromCsrResponse.certificateOwnershipToken,
        parameters: params
    });
    identityResources.thingName = registerThingResponse.thingName;
    expect(registerThingResponse.thingName).toBeDefined();

    context.client.close();

    await context.close();
}

conditional_test(hasTestEnvironment())('identityv2 CSR provisioning mqtt5', async () => {
    await doCsrProvisioningTest(ProtocolVersion.Mqtt5);
});


conditional_test(hasTestEnvironment())('identityv2 CSR provisioning mqtt311', async () => {
    await doCsrProvisioningTest(ProtocolVersion.Mqtt311);
});