// @vitest-environment node
import { describe, it, expect, beforeAll } from 'vitest';
import * as pkijs from 'pkijs';
import * as asn1js from 'asn1js';
import { smimeSign } from '../smime-sign';
import { smimeEncrypt } from '../smime-encrypt';
import { smimeDecrypt, SmimeKeyLockedError, findDecryptionCandidates, normalizeCmsBytes } from '../smime-decrypt';
import { smimeVerify } from '../smime-verify';
import { extractCertificateInfo } from '../certificate-utils';
import type { SmimeKeyRecord } from '../types';

/**
 * Integration tests for S/MIME sign→verify and encrypt→decrypt roundtrips.
 * Uses Node.js crypto (not jsdom) for accurate Web Crypto behavior.
 */

const testMimeBytes = new TextEncoder().encode(
  'Content-Type: text/plain; charset=utf-8\r\n\r\nHello, World!',
);

const cryptoEngine = new pkijs.CryptoEngine({
  crypto: crypto,
  subtle: crypto.subtle,
  name: 'webcrypto',
});

async function buildCert(
  cn: string,
  email: string,
  publicKey: CryptoKey,
  signingPrivateKey: CryptoKey,
): Promise<{ cert: pkijs.Certificate; certDer: ArrayBuffer }> {
  const cert = new pkijs.Certificate();
  cert.version = 2;
  cert.serialNumber = new asn1js.Integer({ value: Math.floor(Math.random() * 100000) });

  cert.issuer.typesAndValues.push(
    new pkijs.AttributeTypeAndValue({
      type: '2.5.4.3',
      value: new asn1js.Utf8String({ value: cn }),
    }),
  );
  cert.subject.typesAndValues.push(
    new pkijs.AttributeTypeAndValue({
      type: '2.5.4.3',
      value: new asn1js.Utf8String({ value: cn }),
    }),
  );
  cert.subject.typesAndValues.push(
    new pkijs.AttributeTypeAndValue({
      type: '1.2.840.113549.1.9.1',
      value: new asn1js.IA5String({ value: email }),
    }),
  );
  cert.notBefore.value = new Date('2024-01-01T00:00:00Z');
  cert.notAfter.value = new Date('2030-12-31T23:59:59Z');

  await cert.subjectPublicKeyInfo.importKey(publicKey, cryptoEngine);

  await cert.sign(signingPrivateKey, 'SHA-256', cryptoEngine);

  const certDer = cert.toSchema(true).toBER(false);
  return { cert, certDer };
}

async function makeKeyRecord(
  id: string,
  email: string,
  certDer: ArrayBuffer,
): Promise<SmimeKeyRecord> {
  const cert = new pkijs.Certificate({
    schema: asn1js.fromBER(certDer).result,
  });
  const info = await extractCertificateInfo(cert, certDer);
  return {
    id,
    email: email.toLowerCase(),
    certificate: certDer,
    certificateChain: [],
    encryptedPrivateKey: new ArrayBuffer(0),
    salt: new ArrayBuffer(0),
    iv: new ArrayBuffer(0),
    kdfIterations: 600000,
    issuer: info.issuer,
    subject: info.subject,
    serialNumber: info.serialNumber,
    notBefore: info.notBefore,
    notAfter: info.notAfter,
    fingerprint: info.fingerprint,
    algorithm: info.algorithm,
    capabilities: info.capabilities,
  };
}

// Signing key pair and cert (RSASSA-PKCS1-v1_5 public key embedded in cert)
let signKeyPair: globalThis.CryptoKeyPair;
let signCertDer: ArrayBuffer;

// Encryption key pair and cert (RSA-OAEP public key embedded in cert)
let encKeyPair: globalThis.CryptoKeyPair;
let encCertDer: ArrayBuffer;
let encKeyRecord: SmimeKeyRecord;

// Second encryption identity for cross-recipient tests
let bobEncKeyPair: globalThis.CryptoKeyPair;
let bobEncCertDer: ArrayBuffer;
let bobKeyRecord: SmimeKeyRecord;

beforeAll(async () => {
  pkijs.setEngine('test', crypto, cryptoEngine);

  // --- Signing identity ---
  signKeyPair = await crypto.subtle.generateKey(
    { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
    true,
    ['sign', 'verify'],
  );
  const signResult = await buildCert('Alice Signer', 'alice@example.com', signKeyPair.publicKey, signKeyPair.privateKey);
  signCertDer = signResult.certDer;

  // --- Encryption identity (Alice) ---
  encKeyPair = await crypto.subtle.generateKey(
    { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
    true,
    ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
  );
  // Self-sign with a temporary signing key
  const tempSignKey = await crypto.subtle.generateKey(
    { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
    true,
    ['sign', 'verify'],
  );
  const encResult = await buildCert('Alice', 'alice@example.com', encKeyPair.publicKey, tempSignKey.privateKey);
  encCertDer = encResult.certDer;
  encKeyRecord = await makeKeyRecord('key-alice-enc', 'alice@example.com', encCertDer);

  // --- Bob encryption identity ---
  bobEncKeyPair = await crypto.subtle.generateKey(
    { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
    true,
    ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
  );
  const bobTempSignKey = await crypto.subtle.generateKey(
    { name: 'RSASSA-PKCS1-v1_5', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
    true,
    ['sign', 'verify'],
  );
  const bobResult = await buildCert('Bob', 'bob@example.com', bobEncKeyPair.publicKey, bobTempSignKey.privateKey);
  bobEncCertDer = bobResult.certDer;
  bobKeyRecord = await makeKeyRecord('key-bob-enc', 'bob@example.com', bobEncCertDer);
});

describe('smimeSign + smimeVerify roundtrip', () => {
  it('signs and verifies a message successfully', async () => {
    const signedBlob = await smimeSign(testMimeBytes, signKeyPair.privateKey, signCertDer);
    expect(signedBlob).toBeInstanceOf(Blob);
    expect(signedBlob.type).toContain('application/pkcs7-mime');

    const cmsBytes = await signedBlob.arrayBuffer();
    const result = await smimeVerify(cmsBytes, 'alice@example.com');

    expect(result.status.isSigned).toBe(true);
    expect(result.status.signatureValid).toBe(true);
    expect(result.status.signerEmailMatch).toBe(true);
    expect(result.status.signerCert).toBeDefined();
    expect(result.status.signerCert!.email).toBe('alice@example.com');

    const innerText = new TextDecoder().decode(result.mimeBytes);
    expect(innerText).toContain('Hello, World!');
  });

  it('reports email mismatch when From differs from signer', async () => {
    const signedBlob = await smimeSign(testMimeBytes, signKeyPair.privateKey, signCertDer);
    const cmsBytes = await signedBlob.arrayBuffer();
    const result = await smimeVerify(cmsBytes, 'evil@attacker.com');

    expect(result.status.isSigned).toBe(true);
    expect(result.status.signerEmailMatch).toBe(false);
  });
});

describe('smimeEncrypt + smimeDecrypt roundtrip', () => {
  it('encrypts and decrypts a message', async () => {
    const encryptedBlob = await smimeEncrypt(
      testMimeBytes,
      [encCertDer],
      encCertDer,
    );
    expect(encryptedBlob).toBeInstanceOf(Blob);
    expect(encryptedBlob.type).toContain('application/pkcs7-mime');

    const cmsBytes = await encryptedBlob.arrayBuffer();
    const unlockedKeys = new Map<string, CryptoKey>();
    unlockedKeys.set(encKeyRecord.id, encKeyPair.privateKey);

    const result = await smimeDecrypt({
      cmsBytes,
      keyRecords: [encKeyRecord],
      unlockedKeys,
    });

    expect(result.mimeBytes).toBeDefined();
    const decryptedText = new TextDecoder().decode(result.mimeBytes);
    expect(decryptedText).toContain('Hello, World!');
    expect(result.keyRecordId).toBe(encKeyRecord.id);
  });

  it('throws when no matching key is available', async () => {
    const encryptedBlob = await smimeEncrypt(
      testMimeBytes,
      [encCertDer],
      encCertDer,
    );
    const cmsBytes = await encryptedBlob.arrayBuffer();

    // Bob's key record doesn't match Alice's encrypted message
    await expect(
      smimeDecrypt({
        cmsBytes,
        keyRecords: [bobKeyRecord],
        unlockedKeys: new Map(),
      }),
    ).rejects.toThrow('No imported S/MIME key matches');
  });
});

describe('SmimeKeyLockedError', () => {
  it('has correct name and keyRecordId', () => {
    const err = new SmimeKeyLockedError('test', 'key-1');
    expect(err.name).toBe('SmimeKeyLockedError');
    expect(err.keyRecordId).toBe('key-1');
    expect(err.message).toBe('test');
    expect(err).toBeInstanceOf(Error);
  });
});

describe('findDecryptionCandidates', () => {
  it('returns empty array for invalid CMS data', () => {
    const garbage = new Uint8Array([0, 1, 2, 3]).buffer;
    const result = findDecryptionCandidates(garbage, [encKeyRecord]);
    expect(result).toEqual([]);
  });
});

describe('smimeVerify edge cases', () => {
  it('throws on invalid ASN.1 data', async () => {
    const garbage = new Uint8Array([0, 1, 2, 3]).buffer;
    await expect(smimeVerify(garbage)).rejects.toThrow();
  });
});

describe('normalizeCmsBytes', () => {
  // Helper: a minimal DER-encoded ASN.1 SEQUENCE (0x30 tag)
  const derBytes = new Uint8Array([0x30, 0x03, 0x02, 0x01, 0x05]);

  it('passes through raw DER unchanged', () => {
    const result = new Uint8Array(normalizeCmsBytes(derBytes.buffer as ArrayBuffer));
    expect(result).toEqual(derBytes);
  });

  it('passes through empty buffer unchanged', () => {
    const result = normalizeCmsBytes(new ArrayBuffer(0));
    expect(result.byteLength).toBe(0);
  });

  it('decodes plain base64 content', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const input = new TextEncoder().encode(b64).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('decodes base64 content with MIME headers', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const mime =
      'Content-Type: application/pkcs7-mime\r\n' +
      'Content-Transfer-Encoding: base64\r\n' +
      '\r\n' +
      b64 + '\r\n';
    const input = new TextEncoder().encode(mime).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('decodes PEM-wrapped content', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const pem = '-----BEGIN PKCS7-----\n' + b64 + '\n-----END PKCS7-----\n';
    const input = new TextEncoder().encode(pem).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('decodes MIME headers with unix line endings', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const mime =
      'Content-Type: application/pkcs7-mime\n' +
      'Content-Transfer-Encoding: base64\n' +
      '\n' +
      b64 + '\n';
    const input = new TextEncoder().encode(mime).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('decodes base64 when MIME headers are very long', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const longHeader = 'X-Long-Header: ' + 'A'.repeat(3000) + '\r\n';
    const mime =
      longHeader +
      'Content-Type: application/pkcs7-mime\r\n' +
      'Content-Transfer-Encoding: base64\r\n' +
      '\r\n' +
      b64 + '\r\n';
    const input = new TextEncoder().encode(mime).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('extracts largest base64 block from multipart-like text', () => {
    const b64 = btoa(String.fromCharCode(...derBytes));
    const multipartLike =
      'Content-Type: multipart/mixed; boundary="b"\r\n\r\n' +
      '--b\r\n' +
      'Content-Type: text/plain\r\n\r\n' +
      'hello\r\n' +
      '--b\r\n' +
      'Content-Type: application/pkcs7-mime\r\n' +
      'Content-Transfer-Encoding: base64\r\n\r\n' +
      b64 + '\r\n' +
      '--b--\r\n';
    const input = new TextEncoder().encode(multipartLike).buffer as ArrayBuffer;
    const result = new Uint8Array(normalizeCmsBytes(input));
    expect(result).toEqual(derBytes);
  });

  it('returns original when content is not decodable', () => {
    const garbage = new Uint8Array([0x01, 0x02, 0xFF, 0xFE]);
    const result = normalizeCmsBytes(garbage.buffer as ArrayBuffer);
    // Should return original since it can\'t be decoded
    expect(result.byteLength).toBeGreaterThan(0);
  });
});
