import assert from 'node:assert/strict';
import {
  chmodSync,
  mkdirSync,
  mkdtempSync,
  readFileSync,
  readdirSync,
  rmSync,
  statSync,
  writeFileSync
} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { spawnSync } from 'node:child_process';
import { test } from 'node:test';

const scriptPath = fileURLToPath(new URL('../scripts/sync-tls-certificate.sh', import.meta.url));
const scriptSource = readFileSync(scriptPath, 'utf8');
const opensslLookup = process.platform === 'win32'
  ? { status: 1, stdout: '' }
  : spawnSync('sh', ['-c', 'command -v openssl'], { encoding: 'utf8' });
const opensslPath = opensslLookup.status === 0 ? opensslLookup.stdout.trim() : '';
const canRun = process.platform !== 'win32' && Boolean(opensslPath);

test('uses protocol-specific STARTTLS probes for explicit TLS upgrade ports', () => {
  assert.match(scriptSource, /25\|587\|2525\) protocol_args=\(-starttls smtp\)/);
  assert.match(scriptSource, /110\).*protocol_args=\(-starttls pop3\)/);
  assert.match(scriptSource, /143\).*protocol_args=\(-starttls imap\)/);
});

test('falls back to docker port when compose cannot resolve the Dovecot mapping', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');
  const fallback = createComposePortFallbackPath(fixture.root, fixture.path);

  const result = runSync(fixture, {
    restart: '1',
    env: {
      PATH: fallback.path,
      MAILHUB_CERT_VERIFY_ENDPOINTS: '993',
      MAILHUB_TEST_DOCKER_LOG: fallback.logFile,
      MAILHUB_TEST_PRESENTED_CERT: fixture.sourceCert,
      MAILHUB_TEST_REAL_OPENSSL: opensslPath
    }
  });

  assert.equal(result.status, 0, result.stderr);
  const commands = readFileSync(fallback.logFile, 'utf8').trim().split('\n');
  assert.ok(commands.includes('compose port dovecot 31993'), commands.join('\n'));
  assert.ok(commands.includes('compose ps -q dovecot'), commands.join('\n'));
  assert.ok(commands.includes('port dovecot-fallback 31993/tcp'), commands.join('\n'));
});

test('atomically synchronizes a valid wildcard certificate and preserves safe permissions', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');

  const first = runSync(fixture);
  assert.equal(first.status, 0, first.stderr);
  assert.match(first.stdout, /MailHub TLS certificate synchronized for mail\.example\.test\./);
  assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
  assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  assert.equal(fileMode(fixture.targetCert), 0o644);
  assert.equal(fileMode(fixture.targetKey), 0o640);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);

  const previousCert = readFileSync(fixture.targetCert);
  const previousKey = readFileSync(fixture.targetKey);
  chmodSync(fixture.targetCert, 0o640);
  chmodSync(fixture.targetKey, 0o600);
  generateCertificate(fixture.sourceDir, 'example.test');

  const replacement = runSync(fixture);
  assert.equal(replacement.status, 0, replacement.stderr);
  assert.deepEqual(readFileSync(`${fixture.targetCert}.previous`), previousCert);
  assert.deepEqual(readFileSync(`${fixture.targetKey}.previous`), previousKey);
  assert.deepEqual(readFileSync(fixture.targetCert), readFileSync(fixture.sourceCert));
  assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
  assert.equal(fileMode(fixture.targetCert), 0o644);
  assert.equal(fileMode(fixture.targetKey), 0o640);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('is idempotent when the synchronized certificate is already current', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');

  const first = runSync(fixture);
  assert.equal(first.status, 0, first.stderr);
  const certBefore = statSnapshot(fixture.targetCert);
  const keyBefore = statSnapshot(fixture.targetKey);

  const second = runSync(fixture);
  assert.equal(second.status, 0, second.stderr);
  assert.match(second.stdout, /MailHub TLS certificate is already up to date\./);
  assert.deepEqual(statSnapshot(fixture.targetCert), certBefore);
  assert.deepEqual(statSnapshot(fixture.targetKey), keyBefore);
  assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('rejects a certificate that does not cover MAIL_HOSTNAME without touching the targets', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'other.test');
  mkdirSync(fixture.certsDir, { recursive: true });
  writeFileSync(fixture.targetCert, 'existing certificate\n');
  writeFileSync(fixture.targetKey, 'existing private key\n');
  chmodSync(fixture.targetCert, 0o640);
  chmodSync(fixture.targetKey, 0o600);
  const certBefore = readFileSync(fixture.targetCert);
  const keyBefore = readFileSync(fixture.targetKey);

  const result = runSync(fixture);
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /source certificate does not cover mail\.example\.test\./);
  assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
  assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
  assert.equal(fileMode(fixture.targetCert), 0o640);
  assert.equal(fileMode(fixture.targetKey), 0o600);
  assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('rejects certificate and private key paths that resolve to the same target', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');
  mkdirSync(fixture.certsDir, { recursive: true });
  writeEnvironment(fixture, {
    SUBMISSION_TLS_KEY: '/certs/fullchain.pem'
  });

  const result = runSync(fixture);
  assert.notEqual(result.status, 0);
  assert.equal(readdirSync(fixture.certsDir).some((name) => name === 'fullchain.pem' || name === 'privkey.pem'), false);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('rejects a mismatched certificate and private key without touching the targets', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  const unrelatedDir = path.join(fixture.root, 'unrelated');
  mkdirSync(unrelatedDir, { recursive: true });
  generateCertificate(fixture.sourceDir, 'example.test');
  generateCertificate(unrelatedDir, 'example.test');
  writeFileSync(fixture.sourceKey, readFileSync(path.join(unrelatedDir, 'privkey.pem')));
  mkdirSync(fixture.certsDir, { recursive: true });
  writeFileSync(fixture.targetCert, 'existing certificate\n');
  writeFileSync(fixture.targetKey, 'existing private key\n');
  const certBefore = readFileSync(fixture.targetCert);
  const keyBefore = readFileSync(fixture.targetKey);

  const result = runSync(fixture);
  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /certificate and private key do not match/i);
  assert.deepEqual(readFileSync(fixture.targetCert), certBefore);
  assert.deepEqual(readFileSync(fixture.targetKey), keyBefore);
  assert.equal(readdirSync(fixture.certsDir).some((name) => name.endsWith('.previous')), false);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('tightens an overly permissive existing private key to mode 0640', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');
  const first = runSync(fixture);
  assert.equal(first.status, 0, first.stderr);

  chmodSync(fixture.targetKey, 0o666);
  generateCertificate(fixture.sourceDir, 'example.test');
  const replacement = runSync(fixture);

  assert.equal(replacement.status, 0, replacement.stderr);
  assert.equal(fileMode(fixture.targetKey), 0o640);
  assert.deepEqual(readFileSync(fixture.targetKey), readFileSync(fixture.sourceKey));
});

test('validates and installs a staged source snapshot when managed files change mid-run', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  const replacementDir = path.join(fixture.root, 'replacement');
  mkdirSync(replacementDir, { recursive: true });
  generateCertificate(fixture.sourceDir, 'example.test');
  generateCertificate(replacementDir, 'example.test');
  const expectedCert = readFileSync(fixture.sourceCert);
  const expectedKey = readFileSync(fixture.sourceKey);
  const marker = path.join(fixture.root, 'source-mutated');
  const mutationPath = createOpenSslMutationPath(fixture.root, fixture.path);

  const result = runSync(fixture, {
    env: {
      PATH: mutationPath,
      MAILHUB_TEST_REAL_OPENSSL: opensslPath,
      MAILHUB_TEST_MUTATION_MARKER: marker,
      MAILHUB_TEST_SOURCE_CERT: fixture.sourceCert,
      MAILHUB_TEST_SOURCE_KEY: fixture.sourceKey,
      MAILHUB_TEST_REPLACEMENT_CERT: path.join(replacementDir, 'fullchain.pem'),
      MAILHUB_TEST_REPLACEMENT_KEY: path.join(replacementDir, 'privkey.pem')
    }
  });

  assert.equal(result.status, 0, result.stderr);
  assert.equal(readFileSync(fixture.targetCert).equals(expectedCert), true, 'installed certificate must use the staged snapshot');
  assert.equal(readFileSync(fixture.targetKey).equals(expectedKey), true, 'installed private key must use the staged snapshot');
  assert.notDeepEqual(readFileSync(fixture.sourceCert), expectedCert);
  assert.notDeepEqual(readFileSync(fixture.sourceKey), expectedKey);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

test('rolls back both certificate files when post-restart health validation fails', { skip: !canRun }, (t) => {
  const fixture = createFixture(t);
  generateCertificate(fixture.sourceDir, 'example.test');
  const first = runSync(fixture);
  assert.equal(first.status, 0, first.stderr);
  const certBefore = readFileSync(fixture.targetCert);
  const keyBefore = readFileSync(fixture.targetKey);
  const certModeBefore = fileMode(fixture.targetCert);
  const keyModeBefore = fileMode(fixture.targetKey);

  generateCertificate(fixture.sourceDir, 'example.test');
  const failurePath = createFailedHealthPath(fixture.root, fixture.path);
  const result = runSync(fixture, {
    restart: '1',
    env: { PATH: failurePath }
  });

  assert.notEqual(result.status, 0);
  assert.match(result.stderr, /did not become healthy/i);
  assert.equal(readFileSync(fixture.targetCert).equals(certBefore), true, 'certificate must roll back after failed health validation');
  assert.equal(readFileSync(fixture.targetKey).equals(keyBefore), true, 'private key must roll back after failed health validation');
  assert.equal(fileMode(fixture.targetCert), certModeBefore);
  assert.equal(fileMode(fixture.targetKey), keyModeBefore);
  assert.deepEqual(stagingFiles(fixture.certsDir), []);
});

function createFixture(t) {
  const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-cert-sync-'));
  const projectDir = path.join(root, 'project');
  const sourceDir = path.join(root, 'source');
  const certsDir = path.join(projectDir, 'certs');
  const envFile = path.join(projectDir, '.env');
  mkdirSync(projectDir, { recursive: true });
  mkdirSync(sourceDir, { recursive: true });
  writeFileSync(envFile, [
    'MAIL_HOSTNAME=mail.example.test',
    'SUBMISSION_TLS_CERT=/certs/fullchain.pem',
    'SUBMISSION_TLS_KEY=/certs/privkey.pem',
    ''
  ].join('\n'));
  t.after(() => rmSync(root, { recursive: true, force: true }));
  return {
    root,
    projectDir,
    sourceDir,
    certsDir,
    envFile,
    sourceCert: path.join(sourceDir, 'fullchain.pem'),
    sourceKey: path.join(sourceDir, 'privkey.pem'),
    targetCert: path.join(certsDir, 'fullchain.pem'),
    targetKey: path.join(certsDir, 'privkey.pem'),
    path: createCompatibilityPath(root)
  };
}

function writeEnvironment(fixture, overrides = {}) {
  const values = {
    MAIL_HOSTNAME: 'mail.example.test',
    SUBMISSION_TLS_CERT: '/certs/fullchain.pem',
    SUBMISSION_TLS_KEY: '/certs/privkey.pem',
    ...overrides
  };
  writeFileSync(fixture.envFile, [
    `MAIL_HOSTNAME=${values.MAIL_HOSTNAME}`,
    `SUBMISSION_TLS_CERT=${values.SUBMISSION_TLS_CERT}`,
    `SUBMISSION_TLS_KEY=${values.SUBMISSION_TLS_KEY}`,
    ''
  ].join('\n'));
}

function generateCertificate(sourceDir, domain) {
  const result = spawnSync('openssl', [
    'req',
    '-x509',
    '-newkey', 'rsa:2048',
    '-nodes',
    '-sha256',
    '-days', '30',
    '-subj', `/CN=*.${domain}`,
    '-addext', `subjectAltName=DNS:*.${domain},DNS:${domain}`,
    '-keyout', path.join(sourceDir, 'privkey.pem'),
    '-out', path.join(sourceDir, 'fullchain.pem')
  ], { encoding: 'utf8' });
  assert.equal(result.status, 0, result.stderr);
}

function runSync(fixture, { restart = '0', env = {} } = {}) {
  const rootOwnershipEnv = typeof process.getuid === 'function' && process.getuid() === 0
    ? { MAILHUB_CERT_READER_GID: String(process.getgid()) }
    : {};
  return spawnSync('bash', [scriptPath], {
    cwd: fixture.projectDir,
    encoding: 'utf8',
    env: {
      ...process.env,
      PATH: fixture.path,
      MAILHUB_CERT_PROJECT_DIR: fixture.projectDir,
      MAILHUB_CERT_ENV_FILE: fixture.envFile,
      MAILHUB_CERT_SOURCE_DIR: fixture.sourceDir,
      MAILHUB_CERT_RESTART: restart,
      ...rootOwnershipEnv,
      ...env
    }
  });
}

function statSnapshot(file) {
  const stat = statSync(file);
  return {
    ino: stat.ino,
    mode: stat.mode & 0o777,
    size: stat.size,
    mtimeMs: stat.mtimeMs
  };
}

function fileMode(file) {
  return statSync(file).mode & 0o777;
}

function stagingFiles(directory) {
  return readdirSync(directory).filter((name) => name.includes('.tmp.') || name.startsWith('.cert-sync.'));
}

function createCompatibilityPath(root) {
  if (process.platform !== 'darwin') return process.env.PATH || '';
  const binDir = path.join(root, 'bin');
  mkdirSync(binDir, { recursive: true });
  writeExecutable(path.join(binDir, 'cp'), stripDoubleDashWrapper('/bin/cp'));
  writeExecutable(path.join(binDir, 'mv'), stripDoubleDashWrapper('/bin/mv'));
  writeExecutable(path.join(binDir, 'rm'), stripDoubleDashWrapper('/bin/rm'));
  writeExecutable(path.join(binDir, 'chmod'), `#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == --reference=* ]]; then
  reference="\${1#*=}"
  shift
  mode="$(/usr/bin/stat -f '%Lp' "\${reference}")"
  exec /bin/chmod "\${mode}" "$@"
fi
exec /bin/chmod "$@"
`);
  writeExecutable(path.join(binDir, 'chown'), `#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == --reference=* ]]; then
  reference="\${1#*=}"
  shift
  owner="$(/usr/bin/stat -f '%u:%g' "\${reference}")"
  exec /usr/sbin/chown "\${owner}" "$@"
fi
exec /usr/sbin/chown "$@"
`);
  writeExecutable(path.join(binDir, 'sha256sum'), `#!/usr/bin/env bash
exec /usr/bin/shasum -a 256 "$@"
`);
  return `${binDir}${path.delimiter}${process.env.PATH || ''}`;
}

function createOpenSslMutationPath(root, basePath) {
  const binDir = path.join(root, 'mutating-openssl-bin');
  mkdirSync(binDir, { recursive: true });
  writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
set -euo pipefail
if [[ ! -e "\${MAILHUB_TEST_MUTATION_MARKER}" ]]; then
  : > "\${MAILHUB_TEST_MUTATION_MARKER}"
  cp "\${MAILHUB_TEST_REPLACEMENT_CERT}" "\${MAILHUB_TEST_SOURCE_CERT}"
  cp "\${MAILHUB_TEST_REPLACEMENT_KEY}" "\${MAILHUB_TEST_SOURCE_KEY}"
fi
exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
`);
  return `${binDir}${path.delimiter}${basePath}`;
}

function createFailedHealthPath(root, basePath) {
  const binDir = path.join(root, 'failed-health-bin');
  mkdirSync(binDir, { recursive: true });
writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
set -euo pipefail
if [[ "$*" == *"tls.createSecureContext"* ]]; then
  exit 0
fi
if [[ "$*" == *"test -r"* ]]; then
  exit 0
fi
if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
  exit 0
fi
exit 1
`);
  writeExecutable(path.join(binDir, 'sleep'), `#!/usr/bin/env bash
exit 0
`);
  return `${binDir}${path.delimiter}${basePath}`;
}

function createComposePortFallbackPath(root, basePath) {
  const binDir = path.join(root, 'compose-port-fallback-bin');
  const logFile = path.join(root, 'compose-port-fallback.log');
  mkdirSync(binDir, { recursive: true });
  writeFileSync(logFile, '');
  writeExecutable(path.join(binDir, 'docker'), `#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "\${MAILHUB_TEST_DOCKER_LOG}"
if [[ "\${1:-}" == "compose" && "\${2:-}" == "exec" ]]; then
  exit 0
fi
if [[ "\${1:-}" == "compose" && "\${2:-}" == "restart" ]]; then
  exit 0
fi
if [[ "\${1:-}" == "compose" && "\${2:-}" == "port" ]]; then
  printf '%s\\n' 'invalid IP:0' >&2
  exit 1
fi
if [[ "\${1:-}" == "compose" && "\${2:-}" == "ps" && "\${3:-}" == "-q" ]]; then
  printf '%s\\n' 'dovecot-fallback'
  exit 0
fi
if [[ "\${1:-}" == "port" && "\${2:-}" == "dovecot-fallback" && "\${3:-}" == "31993/tcp" ]]; then
  printf '%s\\n' '0.0.0.0:1993'
  exit 0
fi
exit 1
`);
  writeExecutable(path.join(binDir, 'openssl'), `#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == "s_client" ]]; then
  connection=""
  previous=""
  for argument in "$@"; do
    if [[ "\${previous}" == "-connect" ]]; then
      connection="\${argument}"
      break
    fi
    previous="\${argument}"
  done
  [[ "\${connection}" == "127.0.0.1:1993" ]] || exit 71
  cat "\${MAILHUB_TEST_PRESENTED_CERT}"
  exit 0
fi
exec "\${MAILHUB_TEST_REAL_OPENSSL}" "$@"
`);
  return {
    logFile,
    path: `${binDir}${path.delimiter}${basePath}`
  };
}

function stripDoubleDashWrapper(command) {
  return `#!/usr/bin/env bash
set -euo pipefail
args=()
for argument in "$@"; do
  [[ "\${argument}" == "--" ]] || args+=("\${argument}")
done
exec ${command} "\${args[@]}"
`;
}

function writeExecutable(file, content) {
  writeFileSync(file, content);
  chmodSync(file, 0o755);
}
