/* eslint-disable @typescript-eslint/no-explicit-any */
/*
 * Copyright (C) 2019 StApps
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import {Logger} from '@openstapps/logger';
import chai from 'chai';
import {expect} from 'chai';
import {ContainerInfo} from 'dockerode';
import {
  sslHardeningParameters,
  protocolHardeningParameters,
  SSLFilePaths,
  configFile,
} from './../src/common.js';
import {
  containerMatchesRegex,
  generateUpstreamMap,
  getGatewayOfStAppsBackend,
  getTemplateView,
  generateListener,
  generateMetricsServer,
  getContainers,
} from '../src/main.js';
import path from 'path';
import {mkdirSync, writeFileSync, unlinkSync, rmdirSync} from 'fs';
import {anyContainerWithExposedPorts} from './containers/any-with-exposed-ports.js';
import {backendContainerWithExposedPorts} from './containers/backend-with-exposed-ports.js';
import {swarmBackendContainerWithExposedPorts} from './containers/swarm-backend-with-exposed-ports.js';
import {fileURLToPath} from 'url';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import portScannerModule from '../src/port-scanner.js';

process.on('unhandledRejection', async error => {
  await Logger.error(error);

  process.exit(1);
});

chai.should();
chai.use(sinonChai);

console.log(configFile);

describe('main', function () {
  this.timeout(1000);
  this.slow(500);

  const sandbox = sinon.createSandbox();

  beforeEach(function () {
    sandbox.restore();
  });

  it('should check if container does not match any container', function () {
    expect(containerMatchesRegex('anyName', new RegExp('d+'), anyContainerWithExposedPorts)).to.be.equal(
      false,
    );
  });

  it('should check if container does not match if version is incorrect', function () {
    expect(
      containerMatchesRegex('backend', new RegExp('1\\.4\\.\\d+'), backendContainerWithExposedPorts),
    ).to.be.equal(false);
  });

  it('should check if container matches', function () {
    expect(
      containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), backendContainerWithExposedPorts),
    ).to.be.equal(true);
    expect(
      containerMatchesRegex('backend', new RegExp('1\\.0\\.\\d+'), swarmBackendContainerWithExposedPorts),
    ).to.be.equal(true);
  });

  it('should get gateway of any container with exposed ports', async function () {
    expect(await getGatewayOfStAppsBackend(anyContainerWithExposedPorts)).to.be.equal('0.0.0.0:80');
  });

  it('should get gateway of backend container', async function () {
    const spy = sandbox.stub(console, 'error');

    const containerWithoutPorts: Partial<ContainerInfo> = {
      Id: 'Foo',
      Ports: [],
      Names: ['/container_name_1'],
    };

    expect(await getGatewayOfStAppsBackend(containerWithoutPorts as ContainerInfo)).to.be.equal('');
    expect(spy).to.have.been.calledWithMatch('Container /container_name_1 does not advertise any port.');
  });

  it('should get gateway of backend container without ports', async function () {
    expect(await getGatewayOfStAppsBackend(backendContainerWithExposedPorts)).to.be.equal('127.0.0.1:3000');
  });

  it('should get gateway of backend container within docker swarm', async function () {
    const backendContainer = swarmBackendContainerWithExposedPorts as any;
    delete backendContainer.Ports[0].IP;

    const spy = sandbox.stub(portScannerModule, 'isPortFree').resolves(true);
    expect(await getGatewayOfStAppsBackend(backendContainer)).to.be.equal('172.18.0.3:3000');
    expect(spy).to.have.been.called;
  });

  it('should fail to get gateway of backend container if unreachable', async function () {
    const backendContainer = swarmBackendContainerWithExposedPorts as any;
    delete backendContainer.Ports[0].IP;

    const spy = sandbox.stub(console, 'error');

    const scanner = sandbox.stub(portScannerModule, 'isPortFree').resolves(false);

    expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal('');
    expect(scanner.calledOnce).to.be.true;
    expect(spy).to.have.been.calledWithMatch(
      "It's possible your current Docker network setup isn't supported yet.",
    );
  });

  it('should fail to get gateway of backend container network mode is unsupported', async function () {
    const backendContainer = swarmBackendContainerWithExposedPorts as any;
    delete backendContainer.Ports[0].IP;
    delete backendContainer.Ports[0].PublicPort;
    delete backendContainer.Ports[0].PrivatePort;

    const spy = sandbox.stub(console, 'error');

    expect(await getGatewayOfStAppsBackend(swarmBackendContainerWithExposedPorts)).to.be.equal('');
    expect(spy).to.have.been.calledWithMatch(
      "It's possible your current Docker network setup isn't supported yet.",
    );
  });

  it('should upstream map calls logger error when no matching container is found', async function () {
    const spy = sandbox.stub(console, 'warn');

    expect(await generateUpstreamMap(['0\\.8\\.\\d+'], ['1\\.1\\.\\d+'], [backendContainerWithExposedPorts]))
      .to.be.equal(`map $http_x_stapps_version $proxyurl {
  default unsupported;
  "~0\\.8\\.\\d+" unavailable;
  "~1\\.1\\.\\d+" outdated;
}
`);

    expect(spy).to.have.been.calledWithMatch('No backend for version');
  });

  it('should upstream map with one active version and no outdated ones', async function () {
    expect(await generateUpstreamMap(['1\\.0\\.\\d+'], ['0\\.8\\.\\d+'], [backendContainerWithExposedPorts]))
      .to.be.equal(`map $http_x_stapps_version $proxyurl {
  default unsupported;
  "~1\\.0\\.\\d+" 1__0___d_;
  "~0\\.8\\.\\d+" outdated;
}
upstream 1__0___d_ {
  server 127.0.0.1:3000;
}
`);
  });

  it('should get containers', async function () {
    try {
      await getContainers();
      return false;
    } catch (error) {
      if ((error as Error).message.startsWith('No')) {
        // Result, if docker is installed
        expect((error as Error).message).to.equal(`No running docker containers found.

Please check if docker is running and Node.js can access the docker socket (/var/run/docker.sock)`);
      } else {
        // Result, if docker is not installed
        expect([
          new Error(`connect ENOENT /var/run/docker.sock`).message,
          new Error('connect EACCES /var/run/docker.sock').message,
        ]).to.include((error as Error).message);
      }
    }

    return true;
  });

  it('should get template view', async function () {
    try {
      const containersWithSameVersion = [backendContainerWithExposedPorts, backendContainerWithExposedPorts];
      await getTemplateView(containersWithSameVersion);
      return false;
    } catch (error) {
      expect((error as Error).message).to.equal(`Multiple backends for one version found.`);
    }

    return true;
  });

  it('should include metrics config', async function () {
    expect(await generateMetricsServer('test', true)).length.to.be.greaterThan(1);
  });

  it('should omit metrics config', async function () {
    expect(await generateMetricsServer('test', false)).to.equal('');
  });

  it('should create listener faulty config', async function () {
    expect(
      generateListener({
        certificate: 'faultyTest',
        certificateChain: 'faultyTest',
        certificateKey: 'faultyTest',
        dhparam: 'faultyTest',
      }),
    ).to.equal(`listen 80 default_server;

${protocolHardeningParameters}
`);
  });

  it('should create listener correct config', async function () {
    const testCertDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'certs');
    mkdirSync(testCertDirectory);

    const certificateFile = path.resolve(testCertDirectory, 'ssl.crt');
    const certificateKeyFile = path.resolve(testCertDirectory, 'ssl.key');
    const certificateChainFile = path.resolve(testCertDirectory, 'chain.crt');
    const dhparamFile = path.resolve(testCertDirectory, 'dhparam.pem');

    writeFileSync(certificateFile, 'Test');
    writeFileSync(certificateKeyFile, 'Test');
    writeFileSync(certificateChainFile, 'Test');
    writeFileSync(dhparamFile, 'Test');

    const sslFilePaths: SSLFilePaths = {
      certificate: certificateFile,
      certificateKey: certificateKeyFile,
      certificateChain: certificateChainFile,
      dhparam: dhparamFile,
    };

    expect(generateListener(sslFilePaths)).to.equal(`    listen 443 ssl default_server;
    ssl_certificate ${sslFilePaths.certificate};
    ssl_certificate_key ${sslFilePaths.certificateKey};
    ssl_trusted_certificate ${sslFilePaths.certificateChain};
    ssl_dhparam ${sslFilePaths.dhparam};
    ${sslHardeningParameters}

${protocolHardeningParameters}
`);

    unlinkSync(certificateFile);
    unlinkSync(certificateKeyFile);
    unlinkSync(certificateChainFile);
    unlinkSync(dhparamFile);
    rmdirSync(testCertDirectory);
  });
});
