import Crypto
import Foundation
import X509
import XCTest
import _CryptoExtras

#if canImport(Security)
    import Security
#endif

@testable import NucleicMDM

final class PKCS12Tests: XCTestCase {
    /// Known answers for the PKCS#12 v1 KDF (SHA-1). These are not self-generated: the same inputs
    /// were run through an independent implementation that was first proven to reproduce openssl's
    /// stored `MacData` HMAC and the key/IV that decrypt an
    /// `openssl pkcs12 -export -legacy` shrouded key bag. A refactor that silently changes the KDF —
    /// the exact failure that produced eleven runs of `-323` — breaks here.
    func testKDFMatchesCrossCheckedKnownAnswers() {
        let salt: [UInt8] = [1, 2, 3, 4, 5, 6, 7, 8]
        XCTAssertEqual(
            PKCS12.pkcs12KDF(
                password: "hunter2", salt: salt, id: 3, iterations: 2048, keyLength: 20),
            [
                0x9e, 0x31, 0xb2, 0x9e, 0x10, 0x31, 0xd2, 0x5e, 0x78, 0x0b,
                0x8d, 0xf6, 0xa3, 0x71, 0x26, 0x7b, 0x4d, 0xbe, 0x1b, 0xbb,
            ], "id=3 (MAC key)")
        XCTAssertEqual(
            PKCS12.pkcs12KDF(
                password: "hunter2", salt: salt, id: 1, iterations: 2048, keyLength: 24),
            [
                0x80, 0x1f, 0x7a, 0xc7, 0xe2, 0x2c, 0xb5, 0x10, 0x3a, 0x47, 0x97, 0x1d,
                0x4b, 0xaf, 0x23, 0x34, 0x19, 0xeb, 0xab, 0x43, 0x1a, 0xc5, 0x22, 0xf5,
            ], "id=1 (3DES key)")
        XCTAssertEqual(
            PKCS12.pkcs12KDF(
                password: "hunter2", salt: salt, id: 2, iterations: 2048, keyLength: 8),
            [0xaa, 0xaf, 0xdc, 0x4e, 0x5f, 0x7d, 0xf4, 0x76], "id=2 (CBC IV)")
    }

    /// FIPS 46-3 known-answer vectors for the raw DES block cipher `TripleDES` is built on. A table
    /// typo anywhere in the permutations or S-boxes fails one of these.
    func testDESKnownAnswerVectors() {
        XCTAssertEqual(
            TripleDES.encryptBlockForTesting(0x0123_4567_89AB_CDEF, key: 0x1334_5779_9BBC_DFF1),
            0x85E8_1354_0F0A_B405)
        XCTAssertEqual(
            TripleDES.encryptBlockForTesting(0x8787_8787_8787_8787, key: 0x0E32_9232_EA6D_0D73),
            0x0000_0000_0000_0000)
    }

    func test3DESRejectsWrongSizedKeyAndIV() {
        XCTAssertThrowsError(
            try TripleDES.encryptCBC([0], key: [UInt8](repeating: 0, count: 16), iv: [UInt8](repeating: 0, count: 8)))
        XCTAssertThrowsError(
            try TripleDES.encryptCBC([0], key: [UInt8](repeating: 0, count: 24), iv: [UInt8](repeating: 0, count: 16)))
    }

    /// 3DES-CBC always pads (PKCS#7), so output is a whole number of blocks and strictly longer than
    /// the input even when the input is already block-aligned.
    func test3DESCBCPadsToWholeBlocks() throws {
        let key = [UInt8](repeating: 0x11, count: 24), iv = [UInt8](repeating: 0x22, count: 8)
        for inputLength in [0, 1, 7, 8, 9, 16] {
            let out = try TripleDES.encryptCBC(
                [UInt8](repeating: 0xAB, count: inputLength), key: key, iv: iv)
            XCTAssertEqual(out.count % 8, 0, "input \(inputLength)")
            XCTAssertGreaterThan(out.count, inputLength, "input \(inputLength)")
        }
    }

    func testExportProducesDERSequence() throws {
        let ca = try MDMCertificateAuthority.generate()
        let identity = try MDMDeviceIdentity.issue(from: ca)
        let blob = try identity.pkcs12()
        XCTAssertEqual(blob.first, 0x30)  // SEQUENCE
        XCTAssertGreaterThan(blob.count, 500)
    }

    /// The load-bearing regression guard. macOS's configuration-profile certificate plugin imports
    /// through `SecItemImport`, which understands only the RFC 7292 v1 algorithms; a "modernised"
    /// PBES2/SHA-256 blob fails with errSecPkcs12VerifyFailure (-25264), surfacing to the user as
    /// `ConfigProfilePluginDomain -323 "The certificate could not be verified (authentication
    /// error)."` — the bug that blocked MDM enrollment for eleven runs. Assert the wire algorithms
    /// directly so the requirement cannot be undone by accident on a machine with no guest to test on.
    func testExportedPKCS12UsesTheAlgorithmsSecItemImportRequires() throws {
        let ca = try MDMCertificateAuthority.generate()
        let identity = try MDMDeviceIdentity.issue(from: ca)
        let blob = [UInt8](try identity.pkcs12())

        func contains(_ oidDER: [UInt8]) -> Bool {
            guard oidDER.count <= blob.count else { return false }
            return (0...(blob.count - oidDER.count)).contains {
                Array(blob[$0..<($0 + oidDER.count)]) == oidDER
            }
        }
        // OID TLVs (tag 0x06): sha1 1.3.14.3.2.26 and pbeWithSHAAnd3-KeyTripleDES-CBC 1.2.840.113549.1.12.1.3
        let sha1: [UInt8] = [0x06, 0x05, 0x2B, 0x0E, 0x03, 0x02, 0x1A]
        let pbe3DES: [UInt8] = [0x06, 0x0A, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x0C, 0x01, 0x03]
        // Must NOT reappear: PBES2 1.2.840.113549.1.5.13, sha256 2.16.840.1.101.3.4.2.1.
        let pbes2: [UInt8] = [0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x05, 0x0D]
        let sha256: [UInt8] = [0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01]

        XCTAssertTrue(contains(sha1), "MacData must use SHA-1; SecItemImport rejects a SHA-256 MAC")
        XCTAssertTrue(contains(pbe3DES), "key bag must use pbeWithSHAAnd3-KeyTripleDES-CBC")
        XCTAssertFalse(contains(pbes2), "PBES2 key bag ⇒ SecItemImport 'Unknown format in import'")
        XCTAssertFalse(contains(sha256), "a SHA-256 MacData ⇒ SecItemImport -25264 ⇒ profile install -323")
    }

    /// Interop: `openssl pkcs12` must accept our blob *and decrypt the key out of it* (proves the real
    /// format, not just our own encoder). `-nodes` forces the shrouded key bag to be decrypted — an
    /// earlier version of this test also passed on "PKCS7 Encrypted data" alone, which does not.
    /// Skipped automatically if openssl is unavailable.
    func testOpenSSLCanParseExportedPKCS12() throws {
        let openssl = "/usr/bin/openssl"
        guard FileManager.default.isExecutableFile(atPath: openssl) else {
            throw XCTSkip("openssl not available at \(openssl)")
        }
        let ca = try MDMCertificateAuthority.generate()
        let identity = try MDMDeviceIdentity.issue(from: ca)
        let blob = try identity.pkcs12()

        let dir = FileManager.default.temporaryDirectory
            .appendingPathComponent("nucleic-mdm-p12-\(UUID().uuidString)")
        try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
        defer { try? FileManager.default.removeItem(at: dir) }
        let p12Path = dir.appendingPathComponent("device.p12")
        try blob.write(to: p12Path)

        // `-info -nodes` walks the whole structure (MAC verify + bag parse + key decrypt). 3DES is in
        // OpenSSL 3's default provider and in LibreSSL, so no `-legacy` is needed to read it back —
        // that is precisely why 3DES and not RC4/RC2. Password via env so it stays out of ps.
        let proc = Process()
        proc.executableURL = URL(fileURLWithPath: openssl)
        proc.arguments = [
            "pkcs12", "-info", "-in", p12Path.path, "-passin", "env:P12PASS", "-nodes",
        ]
        proc.environment = ["P12PASS": identity.password]
        let out = Pipe(), err = Pipe()
        proc.standardOutput = out
        proc.standardError = err
        try proc.run()
        // Read before waiting: `-info` writes the bag walk to stderr and can fill the pipe buffer.
        let outText = String(decoding: out.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
        let errText = String(decoding: err.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
        proc.waitUntilExit()
        XCTAssertEqual(
            proc.terminationStatus, 0,
            "openssl rejected our PKCS#12 (MAC/format error):\n\(errText)")
        XCTAssertTrue(
            outText.contains("PRIVATE KEY"),
            "openssl could not decrypt the shrouded key bag:\n\(outText)\n\(errText)")
        // Only OpenSSL prints a "MAC: <alg>" banner; macOS ships LibreSSL, which does not. Assert it
        // when it is there and stay quiet when it is not — the authoritative algorithm check is
        // `testExportedPKCS12UsesTheAlgorithmsSecItemImportRequires`, which reads the DER directly.
        if let macLine = (outText + errText).split(separator: "\n").first(where: {
            $0.contains("MAC:")
        }) {
            XCTAssertTrue(
                macLine.contains("sha1"),
                "expected a SHA-1 MacData, which is what SecItemImport requires: \(macLine)")
        }
    }

    #if canImport(Security)
        /// The gate that was missing. `SecPKCS12Import` — the only Apple API this codebase used to
        /// touch, via `MDMTLSIdentity` — accepts modern crypto, so it green-lit a blob the guest could
        /// never install. Assert the importer the guest actually uses, and keep asserting the modern
        /// one because the server TLS identity still goes through it.
        func testBothAppleImportersAcceptExportedPKCS12() throws {
            let ca = try MDMCertificateAuthority.generate()
            let identity = try MDMDeviceIdentity.issue(from: ca)
            let blob = try identity.pkcs12()

            // Modern API — used by MDMTLSIdentity.serverIdentity() for the server leaf.
            var modernItems: CFArray?
            let modern = SecPKCS12Import(
                blob as CFData,
                [kSecImportExportPassphrase as String: identity.password] as CFDictionary,
                &modernItems)
            XCTAssertEqual(modern, errSecSuccess, "SecPKCS12Import rejected our PKCS#12")
            XCTAssertEqual((modernItems as? [[String: Any]])?.count, 1)

            // Legacy CDSA API — the one /usr/libexec/mdmclient and ManagedClient link, and therefore
            // the one that decides whether the enrollment profile installs.
            let keychainPath = FileManager.default.temporaryDirectory
                .appendingPathComponent("nucleic-p12-probe-\(UUID().uuidString).keychain").path
            var keychain: SecKeychain?
            let created = SecKeychainCreate(
                keychainPath, UInt32("probe".utf8.count), "probe", false, nil, &keychain)
            guard created == errSecSuccess, let keychain else {
                throw XCTSkip("could not create a probe keychain (status \(created))")
            }
            defer { _ = SecKeychainDelete(keychain) }

            var format: SecExternalFormat = .formatPKCS12
            var itemType: SecExternalItemType = .itemTypeAggregate
            var legacyItems: CFArray?
            var params = SecItemImportExportKeyParameters()
            params.version = UInt32(SEC_KEY_IMPORT_EXPORT_PARAMS_VERSION)
            params.passphrase = Unmanaged<CFTypeRef>.passRetained(
                identity.password as CFString as AnyObject)
            defer { params.passphrase?.release() }
            let legacy = SecItemImport(
                blob as CFData, "device.p12" as CFString, &format, &itemType,
                SecItemImportExportFlags(rawValue: 0), &params, keychain, &legacyItems)
            XCTAssertEqual(
                legacy, errSecSuccess,
                """
                SecItemImport rejected our PKCS#12 (status \(legacy)). This is the failure that \
                surfaces as ConfigProfilePluginDomain -323 "The certificate could not be verified \
                (authentication error)." during profile installation. -25264 means the MacData is not \
                SHA-1; a format error means the key bag is not an RFC 7292 v1 PBE.
                """)
            // Must be a full identity: an unencrypted key bag imports as a bare certificate, which
            // would leave the MDM payload's IdentityCertificateUUID pointing at no identity.
            let kinds = ((legacyItems as? [AnyObject]) ?? []).map { CFGetTypeID($0) }
            XCTAssertEqual(kinds.count, 1)
            XCTAssertEqual(
                kinds.first, SecIdentityGetTypeID(),
                "expected a SecIdentity (cert + private key), not a bare certificate")
        }
    #endif
}
