import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync, existsSync, writeFileSync, statSync, readFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { X509Certificate } from 'crypto';
import { ensureSelfSignedCert } from './self-signed.js';

let dir: string;
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'selfsigned-')); });
afterEach(() => { rmSync(dir, { recursive: true, force: true }); });

describe('ensureSelfSignedCert', () => {
  it('generates a cert+key pair with localhost/127.0.0.1 in the SAN', () => {
    const { cert, key } = ensureSelfSignedCert(dir, []);
    expect(cert).toContain('BEGIN CERTIFICATE');
    expect(key).toContain('PRIVATE KEY');
    const san = new X509Certificate(cert).subjectAltName ?? '';
    expect(san).toContain('localhost');
    expect(san).toContain('127.0.0.1');
  });

  it('includes extra SAN hosts', () => {
    const { cert } = ensureSelfSignedCert(dir, ['maestro.lan']);
    expect(new X509Certificate(cert).subjectAltName ?? '').toContain('maestro.lan');
  });

  it('includes a redirect-style hostname in the SAN (guards server.ts augmentation)', () => {
    // When an operator sets redirect_host: app.lan the server folds that name
    // into selfSignedHosts before calling resolveTlsOptions — this test guards
    // that the augmented host is actually present in the emitted certificate.
    const { cert } = ensureSelfSignedCert(dir, ['redirect.example']);
    expect(new X509Certificate(cert).subjectAltName ?? '').toContain('redirect.example');
  });

  it('reuses an existing valid pair (stable across calls)', () => {
    const a = ensureSelfSignedCert(dir, []);
    const b = ensureSelfSignedCert(dir, []);
    expect(b.cert).toBe(a.cert);
    expect(b.key).toBe(a.key);
  });

  it('regenerates when the existing pair is mismatched/corrupt', () => {
    const a = ensureSelfSignedCert(dir, []);
    writeFileSync(join(dir, 'cert.pem'), 'not a cert');
    const b = ensureSelfSignedCert(dir, []);
    expect(b.cert).toContain('BEGIN CERTIFICATE');
    expect(b.cert).not.toBe(a.cert);
  });

  it('writes the key file with 0600 permissions', () => {
    ensureSelfSignedCert(dir, []);
    expect(statSync(join(dir, 'key.pem')).mode & 0o777).toBe(0o600);
  });

  it('writes the cert file with 0600 permissions', () => {
    ensureSelfSignedCert(dir, []);
    expect(statSync(join(dir, 'cert.pem')).mode & 0o777).toBe(0o600);
  });

  it('treats host:port as a DNS name, not an IP address (regression for isIP fix)', () => {
    const { cert } = ensureSelfSignedCert(dir, ['myhost:8443']);
    const san = new X509Certificate(cert).subjectAltName ?? '';
    // Must appear as a DNS-typed SAN entry, not an IP Address entry
    expect(san).toContain('DNS:myhost:8443');
    expect(san).not.toContain('IP Address:myhost:8443');
  });

  it('always includes localhost even when hostname detection is junk', () => {
    const { cert } = ensureSelfSignedCert(dir, [], { hostname: '' });
    expect(new X509Certificate(cert).subjectAltName ?? '').toContain('localhost');
  });
});
