// SPDX-License-Identifier: Apache-2.0
import { BaseCommandTest } from './base-command-test.js';
import { Flags } from '../../../../src/commands/flags.js';
import { main } from '../../../../src/index.js';
import { Duration } from '../../../../src/core/time/duration.js';
import { NamespaceName } from '../../../../src/types/namespace/namespace-name.js';
import { InjectTokens } from '../../../../src/core/dependency-injection/inject-tokens.js';
import { sleep } from '../../../../src/core/helpers.js';
import http from 'node:http';
import { expect } from 'chai';
import { container } from 'tsyringe-neo';
import { MirrorCommandDefinition } from '../../../../src/commands/command-definitions/mirror-command-definition.js';
import * as constants from '../../../../src/core/constants.js';
import fs from 'node:fs';
import { ShellRunner } from '../../../../src/core/shell-runner.js';
import { ConsensusNodeTest } from './consensus-node-test.js';
import { Repository } from '../../../../src/integration/helm/model/repository.js';
import { Chart } from '../../../../src/integration/helm/model/chart.js';
import { InstallChartOptionsBuilder } from '../../../../src/integration/helm/model/install/install-chart-options-builder.js';
import { ContainerReference } from '../../../../src/integration/kube/resources/container/container-reference.js';
import { ContainerName } from '../../../../src/integration/kube/resources/container/container-name.js';
import { PodName } from '../../../../src/integration/kube/resources/pod/pod-name.js';
import { PodReference } from '../../../../src/integration/kube/resources/pod/pod-reference.js';
export class MirrorNodeTest extends BaseCommandTest {
    static soloMirrorNodeDeployArgv(testName, deployment, clusterReference, pinger) {
        const { newArgv, argvPushGlobalFlags, optionFromFlag } = MirrorNodeTest;
        const argv = newArgv();
        argv.push(MirrorCommandDefinition.COMMAND_NAME, MirrorCommandDefinition.NODE_SUBCOMMAND_NAME, MirrorCommandDefinition.NODE_ADD, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.clusterRef), clusterReference, optionFromFlag(Flags.enableIngress));
        if (pinger) {
            argv.push(optionFromFlag(Flags.pinger));
        }
        argvPushGlobalFlags(argv, testName, true, true);
        return argv;
    }
    static soloMirrorNodeDestroyArgv(testName, deployment, clusterReference) {
        const { newArgv, argvPushGlobalFlags, optionFromFlag } = MirrorNodeTest;
        const argv = newArgv();
        argv.push(MirrorCommandDefinition.COMMAND_NAME, MirrorCommandDefinition.NODE_SUBCOMMAND_NAME, MirrorCommandDefinition.NODE_DESTROY, optionFromFlag(Flags.deployment), deployment, optionFromFlag(Flags.clusterRef), clusterReference, optionFromFlag(Flags.force), optionFromFlag(Flags.quiet), optionFromFlag(Flags.devMode));
        argvPushGlobalFlags(argv, testName, false, true);
        return argv;
    }
    static async forwardRestServicePort(contexts, namespace) {
        const k8Factory = container.resolve(InjectTokens.K8Factory);
        const lastContext = contexts?.length ? contexts[contexts?.length - 1] : undefined;
        const k8 = k8Factory.getK8(lastContext);
        const mirrorNodeRestPods = await k8
            .pods()
            .list(namespace, ['app.kubernetes.io/name=rest', 'app.kubernetes.io/component=rest']);
        expect(mirrorNodeRestPods).to.have.lengthOf(1);
        const portForwarder = await k8
            .pods()
            .readByReference(mirrorNodeRestPods[0].podReference)
            .portForward(5551, 5551);
        await sleep(Duration.ofSeconds(2));
        return portForwarder;
    }
    static async stopPortForward(contexts, portForwarder) {
        const k8Factory = container.resolve(InjectTokens.K8Factory);
        const k8 = k8Factory.getK8(contexts[contexts.length]);
        // eslint-disable-next-line unicorn/no-null
        await k8.pods().readByReference(null).stopPortForward(portForwarder);
    }
    static async verifyMirrorNodeDeployWasSuccessful(contexts, namespace, testLogger, createdAccountIds, consensusNodesCount) {
        const portForwarder = await MirrorNodeTest.forwardRestServicePort(contexts, namespace);
        try {
            const queryUrl = 'http://localhost:5551/api/v1/network/nodes';
            let received = false;
            // wait until the transaction reached consensus and retrievable from the mirror node API
            while (!received) {
                const request = http.request(queryUrl, { method: 'GET', timeout: 100, headers: { Connection: 'close' } }, (response) => {
                    response.setEncoding('utf8');
                    response.on('data', (chunk) => {
                        // convert chunk to json object
                        const object = JSON.parse(chunk);
                        expect(object.nodes?.length, `expect there to be ${consensusNodesCount} nodes in the mirror node's copy of the address book`).to.equal(consensusNodesCount);
                        expect(object.nodes[0].service_endpoints?.length, 'expect there to be at least one service endpoint').to.be.greaterThan(0);
                        received = true;
                    });
                });
                request.on('error', (error) => {
                    testLogger.debug(`problem with request: ${error.message}`, error);
                });
                request.end(); // make the request
                await sleep(Duration.ofSeconds(2));
            }
            for (const accountId of createdAccountIds) {
                const accountQueryUrl = `http://localhost:5551/api/v1/accounts/${accountId}`;
                received = false;
                // wait until the transaction reached consensus and retrievable from the mirror node API
                while (!received) {
                    const request = http.request(accountQueryUrl, { method: 'GET', timeout: 100, headers: { Connection: 'close' } }, (response) => {
                        response.setEncoding('utf8');
                        response.on('data', (chunk) => {
                            // convert chunk to json object
                            const object = JSON.parse(chunk);
                            expect(object.account, 'expect the created account to exist in the mirror nodes copy of the accounts').to.equal(accountId);
                            received = true;
                        });
                    });
                    request.on('error', (error) => {
                        testLogger.debug(`problem with request: ${error.message}`, error);
                    });
                    request.end(); // make the request
                    await sleep(Duration.ofSeconds(2));
                }
                await sleep(Duration.ofSeconds(1));
            }
        }
        finally {
            if (portForwarder) {
                await MirrorNodeTest.stopPortForward(contexts, portForwarder);
            }
        }
    }
    static async verifyPingerStatus(contexts, namespace, pingerIsEnabled) {
        const portForwarder = await MirrorNodeTest.forwardRestServicePort(contexts, namespace);
        try {
            const transactionsEndpoint = 'http://localhost:5551/api/v1/transactions';
            // force to fetch new data instead of using cache
            const fetchOptions = {
                cache: 'no-cache',
                headers: {
                    'Cache-Control': 'no-cache, no-store, must-revalidate',
                    Pragma: 'no-cache',
                    Expires: '0',
                },
            };
            const firstResponse = await fetch(transactionsEndpoint, fetchOptions);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const firstData = await firstResponse.json();
            console.log('\r::group::Mirror node verify pinger status first data');
            console.log(`firstData = ${JSON.stringify(firstData, undefined, 2)}`);
            console.log('\r::endgroup::');
            await sleep(Duration.ofSeconds(15));
            const secondResponse = await fetch(transactionsEndpoint, fetchOptions);
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            const secondData = await secondResponse.json();
            console.log('\r::group::Mirror node verify pinger status second data');
            console.log(`secondData = ${JSON.stringify(secondData, undefined, 2)}`);
            console.log('\r::endgroup::');
            expect(firstData.transactions).to.not.be.undefined;
            expect(firstData.transactions.length).to.be.gt(0);
            expect(secondData.transactions).to.not.be.undefined;
            expect(secondData.transactions.length).to.be.gt(0);
            // if pinger is enabled, the first transaction in the first response should not equal the first transaction in the second response
            // if pinger is disabled, the first transaction in the first response should equal the first transaction in the second response
            // if there is more than one transaction in the second response, compare to the second transaction instead of the first
            let secondTransactionIndex = 0;
            if (secondData.transactions.length > 1) {
                secondTransactionIndex = 1;
            }
            if (pingerIsEnabled) {
                expect(firstData.transactions[0]).to.not.deep.equal(secondData.transactions[secondTransactionIndex]);
            }
            else {
                expect(firstData.transactions[0]).to.deep.equal(secondData.transactions[secondTransactionIndex]);
            }
        }
        finally {
            if (portForwarder) {
                await MirrorNodeTest.stopPortForward(contexts, portForwarder);
            }
        }
    }
    static add(options) {
        const { testName, testLogger, deployment, contexts, namespace, clusterReferenceNameArray, createdAccountIds, consensusNodesCount, pinger, } = options;
        const { soloMirrorNodeDeployArgv, verifyMirrorNodeDeployWasSuccessful, verifyPingerStatus } = MirrorNodeTest;
        it(`${testName}: mirror node add`, async () => {
            await main(soloMirrorNodeDeployArgv(testName, deployment, clusterReferenceNameArray[1], pinger));
            await verifyMirrorNodeDeployWasSuccessful(contexts, namespace, testLogger, createdAccountIds, consensusNodesCount);
            await verifyPingerStatus(contexts, namespace, pinger);
        }).timeout(Duration.ofMinutes(10).toMillis());
    }
    static destroy(options) {
        const { testName, deployment, clusterReferenceNameArray } = options;
        const { soloMirrorNodeDestroyArgv } = MirrorNodeTest;
        it(`${testName}: mirror node destroy`, async () => {
            await main(soloMirrorNodeDestroyArgv(testName, deployment, clusterReferenceNameArray[1]));
        }).timeout(Duration.ofMinutes(5).toMillis());
    }
    static postgresPassword = 'XXXXXXX';
    static postgresUsername = 'postgres';
    static postgresReadonlyUsername = 'readonlyuser';
    static postgresReadonlyPassword = 'XXXXXXXX';
    static postgresHostFqdn = 'my-postgresql.database.svc.cluster.local';
    static nameSpace = 'database';
    static postgresName = 'my-postgresql';
    static postgresContainerName = `${this.postgresName}-0`;
    static postgresMirrorNodeDatabaseName = 'mirror_node';
    static getPostgresContainer(k8) {
        return k8
            .containers()
            .readByRef(ContainerReference.of(PodReference.of(NamespaceName.of(this.nameSpace), PodName.of(this.postgresContainerName)), ContainerName.of('postgresql')));
    }
    /**
     * Grants the readonly role to mirror_rest so the REST service can SELECT from tables
     * created by Flyway migrations after V1.0.
     *
     * The importer's V1.0__Init.sql creates mirror_rest without the readonly role, so it
     * has no access to any table added after that migration.  The init.sh script sets default
     * privileges that automatically grant SELECT on new tables to the readonly role; granting
     * readonly to mirror_rest propagates those privileges.
     *
     * This must be called after main() returns (importer pod ready = migrations complete =
     * mirror_rest exists) and before verifyMirrorNodeDeployWasSuccessful.
     */
    static async grantReadonlyRoleToMirrorRestUser(k8) {
        // Use a dollar-quoted block so the grant is safe even if mirror_rest already has the role.
        const grantSql = "DO $grant$ BEGIN IF EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'readonly') " +
            "AND EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'mirror_rest') " +
            'THEN GRANT readonly TO mirror_rest; END IF; END $grant$;';
        const postgresContainer = MirrorNodeTest.getPostgresContainer(k8);
        await postgresContainer.execContainer([
            'env',
            `PGPASSWORD=${MirrorNodeTest.postgresPassword}`,
            'psql',
            '-U',
            MirrorNodeTest.postgresUsername,
            '-d',
            MirrorNodeTest.postgresMirrorNodeDatabaseName,
            '-c',
            grantSql,
        ]);
    }
    static deployWithExternalDatabase(options) {
        const { testName, testLogger, deployment, contexts, namespace, clusterReferenceNameArray, createdAccountIds, consensusNodesCount, pinger, } = options;
        const { soloMirrorNodeDeployArgv, verifyMirrorNodeDeployWasSuccessful, verifyPingerStatus, optionFromFlag } = MirrorNodeTest;
        it(`${testName}: mirror node deploy with external database`, async () => {
            const argv = soloMirrorNodeDeployArgv(testName, deployment, clusterReferenceNameArray[1], pinger);
            process.env.USE_MIRROR_NODE_LEGACY_RELEASE_NAME = 'true';
            // Add external database flags
            argv.push(optionFromFlag(Flags.enableIngress), optionFromFlag(Flags.useExternalDatabase), optionFromFlag(Flags.externalDatabaseHost), this.postgresHostFqdn, optionFromFlag(Flags.externalDatabaseOwnerUsername), this.postgresUsername, optionFromFlag(Flags.externalDatabaseOwnerPassword), this.postgresPassword, optionFromFlag(Flags.externalDatabaseReadonlyUsername), this.postgresReadonlyUsername, optionFromFlag(Flags.externalDatabaseReadonlyPassword), this.postgresReadonlyPassword);
            await main(argv);
            // The importer's V1.0__Init.sql migration creates the mirror_rest user without the readonly
            // role, so it lacks SELECT on tables created after V1.0 (e.g. entity, transaction, node).
            // Grant the readonly role now (after importer pod is ready = migrations are complete).
            const k8Factory = container.resolve(InjectTokens.K8Factory);
            const k8 = k8Factory.getK8(contexts[1]);
            await MirrorNodeTest.grantReadonlyRoleToMirrorRestUser(k8);
            await verifyMirrorNodeDeployWasSuccessful(contexts, namespace, testLogger, createdAccountIds, consensusNodesCount);
            await verifyPingerStatus(contexts, namespace, pinger);
        }).timeout(Duration.ofMinutes(10).toMillis());
        it('Enable port-forward for mirror node gRPC', async () => {
            const k8Factory = container.resolve(InjectTokens.K8Factory);
            const k8 = k8Factory.getK8(contexts[1]);
            const mirrorNodePods = await k8
                .pods()
                .list(namespace, ['app.kubernetes.io/name=grpc', 'app.kubernetes.io/component=grpc']);
            const mirrorNodePod = mirrorNodePods[0];
            await k8.pods().readByReference(mirrorNodePod.podReference).portForward(5600, 5600);
        });
    }
    static installPostgres(options) {
        const { contexts } = options;
        it('should install postgres chart', async () => {
            const k8Factory = container.resolve(InjectTokens.K8Factory);
            k8Factory.getK8(contexts[1]).contexts().updateCurrent(contexts[1]);
            const helm = container.resolve(InjectTokens.Helm);
            await helm.addRepository(new Repository('postgresql-helm', 'https://leverages.github.io/helm'));
            await helm.installChart('my-postgresql', new Chart('postgresql', 'postgresql-helm'), InstallChartOptionsBuilder.builder()
                .set(['deploymentType=local', `postgresql.auth.password=${this.postgresPassword}`])
                .namespace(this.nameSpace)
                .createNamespace(true)
                .kubeContext(contexts[1])
                .build());
            const k8 = k8Factory.getK8(contexts[1]);
            await k8
                .pods()
                .waitForReadyStatus(NamespaceName.of(this.nameSpace), ['app.kubernetes.io/name=postgresql'], constants.PODS_READY_MAX_ATTEMPTS, constants.PODS_READY_DELAY);
            const initScriptPath = 'scripts/external-database/init.sh';
            // check if initScriptPath exist, otherwise throw error
            if (!fs.existsSync(initScriptPath)) {
                throw new Error(`Init script not found at path: ${initScriptPath}`);
            }
            const postgresContainer = MirrorNodeTest.getPostgresContainer(k8);
            await postgresContainer.copyTo(initScriptPath, '/tmp');
            await postgresContainer.execContainer(['chmod', '+x', '/tmp/init.sh']);
            await postgresContainer.execContainer([
                '/bin/bash',
                '/tmp/init.sh',
                this.postgresUsername,
                this.postgresReadonlyUsername,
                this.postgresReadonlyPassword,
            ]);
        }).timeout(Duration.ofMinutes(2).toMillis());
    }
    static runSql(options) {
        it('should run SQL command', async () => {
            const { testCacheDirectory, contexts } = options;
            const k8Factory = container.resolve(InjectTokens.K8Factory);
            const k8 = k8Factory.getK8(contexts[1]);
            const postgresContainer = MirrorNodeTest.getPostgresContainer(k8);
            await postgresContainer.copyTo(`${testCacheDirectory}/database-seeding-query.sql`, '/tmp');
            await postgresContainer.execContainer([
                '/bin/bash',
                '-c',
                `PGPASSWORD=${this.postgresPassword} psql -U ${this.postgresUsername} -f /tmp/database-seeding-query.sql -d ${this.postgresMirrorNodeDatabaseName}`,
            ]);
        });
    }
    static pullAddressBook(options) {
        it('should pull address book from mirror node', async () => {
            const srv = await MirrorNodeTest.forwardRestServicePort(options.contexts, options.namespace);
            const stdOut = await new ShellRunner().run(`curl http://localhost:${srv}/api/v1/network/nodes`);
            const addressBook = JSON.parse(stdOut[0]);
            expect(addressBook.nodes.length).to.be.greaterThan(0);
            const node1 = addressBook.nodes.find((node) => node.node_id === 0);
            // Validate Node 1 data
            expect(node1.grpc_proxy_endpoint.domain_name).to.equal(ConsensusNodeTest.firstNodeCustomGrpcWebEndpointAddress);
            expect(node1.grpc_proxy_endpoint.port).to.equal(ConsensusNodeTest.firstNodeCustomGrpcWebEndpointPort);
            const node2 = addressBook.nodes.find((node) => node.node_id === 1);
            // Validate Node 2 data
            expect(node2.grpc_proxy_endpoint.domain_name).to.equal(ConsensusNodeTest.secondNodeCustomGrpcWebEndpointAddress);
            expect(node2.grpc_proxy_endpoint.port).to.equal(ConsensusNodeTest.secondNodeCustomGrpcWebEndpointPort);
            await MirrorNodeTest.stopPortForward(options.contexts, srv);
        });
    }
}
//# sourceMappingURL=mirror-node-test.js.map