package handler

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/des" //nolint:gosec // RFC 8894 §3.5.2 legacy fallback for backward-compat test
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/asn1"
	"encoding/pem"
	"io"
	"math/big"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/certctl-io/certctl/internal/domain"
	"github.com/certctl-io/certctl/internal/pkcs7"
)

// SCEP RFC 8894 + Intune master bundle Phase 5.2: ChromeOS-shape integration
// tests for the SCEP handler's full RFC 8894 path.
//
// Each test builds a real PKIMessage (acting as the ChromeOS client),
// POSTs it through the handler, and verifies the response. The "client"
// is built from primitives in internal/pkcs7/ — the same builders the
// handler uses on the response side. This is intentional: if the handler
// regresses, the client builder might also regress, and the E2E would
// pass anyway (false negative). The mitigation: round-trip property
// tests in internal/pkcs7/ assert Build/Parse symmetry independently,
// and the handler-side tests focus on the dispatch + status-code wire
// shape rather than the bytes themselves.

// chromeOSStackFixture holds the materials needed for an end-to-end
// ChromeOS SCEP test: an issuer + RA pair (server side), a transient
// device cert (client side), and a constructed SCEPHandler.
type chromeOSStackFixture struct {
	raKey      *rsa.PrivateKey
	raCert     *x509.Certificate
	deviceKey  *rsa.PrivateKey
	deviceCert *x509.Certificate
	handler    SCEPHandler
	svc        *chromeOSMockSCEPService
}

// chromeOSMockSCEPService is the per-test SCEPService implementation used
// by these E2E tests. Records the last call's envelope + CSR for assertion.
type chromeOSMockSCEPService struct {
	caCertPEM              string
	pkcsReqEnvelope        *domain.SCEPRequestEnvelope
	pkcsReqCSRPEM          string
	pkcsReqChallenge       string
	renewalReqEnvelope     *domain.SCEPRequestEnvelope
	renewalReqCSRPEM       string
	getCertInitialEnvelope *domain.SCEPRequestEnvelope
	enrollResult           *domain.SCEPEnrollResult
	failChallenge          bool
}

func (m *chromeOSMockSCEPService) GetCACaps(_ context.Context) string {
	return "POSTPKIOperation\nSHA-256\nSHA-512\nAES\nSCEPStandard\nRenewal\n"
}

func (m *chromeOSMockSCEPService) GetCACert(_ context.Context) (string, error) {
	return m.caCertPEM, nil
}

func (m *chromeOSMockSCEPService) PKCSReq(_ context.Context, _, _, _ string) (*domain.SCEPEnrollResult, error) {
	return m.enrollResult, nil
}

func (m *chromeOSMockSCEPService) PKCSReqWithEnvelope(_ context.Context, csrPEM, challengePassword string, env *domain.SCEPRequestEnvelope) *domain.SCEPResponseEnvelope {
	m.pkcsReqEnvelope = env
	m.pkcsReqCSRPEM = csrPEM
	m.pkcsReqChallenge = challengePassword
	if m.failChallenge {
		return nil
	}
	return &domain.SCEPResponseEnvelope{
		Status:         domain.SCEPStatusSuccess,
		Result:         m.enrollResult,
		TransactionID:  env.TransactionID,
		RecipientNonce: env.SenderNonce,
	}
}

func (m *chromeOSMockSCEPService) RenewalReqWithEnvelope(_ context.Context, csrPEM, _ string, env *domain.SCEPRequestEnvelope) *domain.SCEPResponseEnvelope {
	m.renewalReqEnvelope = env
	m.renewalReqCSRPEM = csrPEM
	return &domain.SCEPResponseEnvelope{
		Status:         domain.SCEPStatusSuccess,
		Result:         m.enrollResult,
		TransactionID:  env.TransactionID,
		RecipientNonce: env.SenderNonce,
	}
}

func (m *chromeOSMockSCEPService) GetCertInitialWithEnvelope(_ context.Context, env *domain.SCEPRequestEnvelope) *domain.SCEPResponseEnvelope {
	m.getCertInitialEnvelope = env
	return &domain.SCEPResponseEnvelope{
		Status:         domain.SCEPStatusFailure,
		FailInfo:       domain.SCEPFailBadCertID,
		TransactionID:  env.TransactionID,
		RecipientNonce: env.SenderNonce,
	}
}

// newChromeOSStackFixture wires up an RA pair + device cert + handler with
// an enroll-result fixture so the test can POST a PKIMessage and verify the
// CertRep response.
func newChromeOSStackFixture(t *testing.T) *chromeOSStackFixture {
	t.Helper()
	raKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey RA: %v", err)
	}
	raCert := selfSignedRSACert(t, raKey, "ra-test")
	deviceKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey device: %v", err)
	}
	deviceCert := selfSignedRSACert(t, deviceKey, "device-transient")

	svc := &chromeOSMockSCEPService{
		enrollResult: &domain.SCEPEnrollResult{
			CertPEM: pemEncodeCert(selfSignedRSACertRaw(t, deviceKey, "issued.example.com")),
		},
	}
	handler := NewSCEPHandler(svc)
	handler.SetRAPair(raCert, raKey)

	return &chromeOSStackFixture{
		raKey:      raKey,
		raCert:     raCert,
		deviceKey:  deviceKey,
		deviceCert: deviceCert,
		handler:    handler,
		svc:        svc,
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_E2E exercises the full RFC 8894 path:
// build a PKIMessage shaped like ChromeOS sends (SignedData wrapping
// EnvelopedData wrapping a CSR, with signerInfo POPO over auth attrs);
// POST through the handler; verify the response is a valid CertRep
// PKIMessage with the issued cert encrypted to the test's transient pubkey.
func TestSCEPHandler_ChromeOSPKIMessage_E2E(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypePKCSReq, "txn-chromeos-e2e", "shared-secret-123", "device-cert.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))

	w, body := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusOK {
		t.Fatalf("POST PKIOperation: got %d, want 200 (body=%q)", w.Code, body)
	}
	if got := w.Header().Get("Content-Type"); got != "application/x-pki-message" {
		t.Errorf("Content-Type = %q, want application/x-pki-message", got)
	}
	if fix.svc.pkcsReqEnvelope == nil {
		t.Fatal("PKCSReqWithEnvelope was not called — handler skipped RFC 8894 path?")
	}
	if fix.svc.pkcsReqEnvelope.TransactionID != "txn-chromeos-e2e" {
		t.Errorf("envelope.TransactionID = %q, want txn-chromeos-e2e", fix.svc.pkcsReqEnvelope.TransactionID)
	}
	if fix.svc.pkcsReqChallenge != "shared-secret-123" {
		t.Errorf("challengePassword = %q, want shared-secret-123", fix.svc.pkcsReqChallenge)
	}
	// Parse the CertRep back via the same builders the handler emits.
	certRep, err := pkcs7.ParseSignedData(body)
	if err != nil {
		t.Fatalf("ParseSignedData(CertRep response): %v", err)
	}
	if len(certRep.SignerInfos) != 1 {
		t.Fatalf("CertRep has %d signers, want 1", len(certRep.SignerInfos))
	}
	if err := certRep.SignerInfos[0].VerifySignature(); err != nil {
		t.Errorf("CertRep RA signature invalid: %v", err)
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_RenewalReq exercises RenewalReq
// dispatch — the handler should route to RenewalReqWithEnvelope based on
// the messageType auth-attr.
func TestSCEPHandler_ChromeOSPKIMessage_RenewalReq(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypeRenewalReq, "txn-renewal-1", "shared-secret-123", "renewal.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))

	w, _ := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusOK {
		t.Fatalf("POST PKIOperation (renewal): got %d, want 200", w.Code)
	}
	if fix.svc.renewalReqEnvelope == nil {
		t.Fatal("RenewalReqWithEnvelope was not called — dispatch missed messageType=17")
	}
	if fix.svc.pkcsReqEnvelope != nil {
		t.Errorf("PKCSReqWithEnvelope was called for a RenewalReq messageType — wrong dispatch")
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_GetCertInitial exercises the polling
// path. v1 always returns FAILURE+badCertID; this test asserts that's what
// ChromeOS sees when it polls.
func TestSCEPHandler_ChromeOSPKIMessage_GetCertInitial(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypeGetCertInitial, "txn-poll-1", "shared-secret-123", "poll.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))

	w, body := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusOK {
		t.Fatalf("POST PKIOperation (poll): got %d, want 200 (body=%q)", w.Code, body)
	}
	if fix.svc.getCertInitialEnvelope == nil {
		t.Fatal("GetCertInitialWithEnvelope was not called — dispatch missed messageType=20")
	}
	// The response should be a CertRep with pkiStatus=2 (FAILURE) +
	// failInfo=4 (badCertID).
	certRep, err := pkcs7.ParseSignedData(body)
	if err != nil {
		t.Fatalf("ParseSignedData: %v", err)
	}
	if len(certRep.SignerInfos) == 0 {
		t.Fatal("CertRep has no signerInfos")
	}
	si := certRep.SignerInfos[0]
	statusRV, ok := si.AuthAttributes[pkcs7.OIDSCEPPKIStatus.String()]
	if !ok {
		t.Fatal("CertRep missing pkiStatus auth-attr")
	}
	statusStr := decodeFirstSetMember(t, statusRV)
	if statusStr != string(domain.SCEPStatusFailure) {
		t.Errorf("pkiStatus = %q, want %q (FAILURE)", statusStr, domain.SCEPStatusFailure)
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_BadPOPO builds a PKIMessage with the
// signerInfo signature corrupted; expects the handler to fall through to
// the MVP path (the RFC 8894 verifier rejects the message, and the MVP
// path also rejects it because the encrypted EnvelopedData isn't a raw
// CSR). Result: HTTP 400 with a clear error message.
func TestSCEPHandler_ChromeOSPKIMessage_BadPOPO(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypePKCSReq, "txn-bad-popo", "shared-secret-123", "bad.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))
	// Tamper with the LAST byte of the message (which lands inside the
	// signature OCTET STRING for a non-trivial chance of corrupting the
	// signature without breaking the outer DER framing).
	pkiMessage[len(pkiMessage)-1] ^= 0xff

	w, _ := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusBadRequest && w.Code != http.StatusOK {
		t.Errorf("POST PKIOperation (bad POPO): got %d, want 400 (MVP fall-through rejection) or 200 (CertRep+failInfo)", w.Code)
	}
	if fix.svc.pkcsReqEnvelope != nil {
		t.Errorf("PKCSReqWithEnvelope was called despite invalid signerInfo signature — POPO check failed open")
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_AESVariants exercises AES-128, 192,
// and 256-CBC. ChromeOS picks based on the GetCACaps response; verify
// all three round-trip correctly.
func TestSCEPHandler_ChromeOSPKIMessage_AESVariants(t *testing.T) {
	cases := []struct {
		name string
		oid  asn1.ObjectIdentifier
	}{
		{"AES-128-CBC", pkcs7.OIDAES128CBC},
		{"AES-192-CBC", pkcs7.OIDAES192CBC},
		{"AES-256-CBC", pkcs7.OIDAES256CBC},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			fix := newChromeOSStackFixture(t)
			pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypePKCSReq, "txn-aes-"+tc.name, "shared-secret-123", "aes.example.com", aesKeyForOID(tc.oid))
			pkiMessage = withContentEncryptionOID(t, pkiMessage, fix, tc.oid, aesKeyForOID(tc.oid))
			w, body := postPKIOperation(t, fix.handler, pkiMessage)
			if w.Code != http.StatusOK {
				t.Fatalf("POST PKIOperation (%s): got %d, want 200 (body=%q)", tc.name, w.Code, body)
			}
		})
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_RAKeyMismatch — closure-bundle
// gap M-1 / acceptance D.1 (the project's SCEP gap-closure spec).
// Build a PKIMessage encrypted to a freshly-generated RA cert whose
// matching private key the server does NOT have. The handler MUST
// reject (RFC 8894 path can't decrypt → falls through; MVP path can't
// either because the EnvelopedData isn't a raw CSR). Assert no
// PKCSReqWithEnvelope was reached. Closes the documented threat that
// an attacker who swaps the RA cert in transit gets a polite error
// rather than information leak about the underlying issuer.
func TestSCEPHandler_ChromeOSPKIMessage_RAKeyMismatch(t *testing.T) {
	fix := newChromeOSStackFixture(t)

	// Build a PKIMessage targeting an UNRELATED RA cert (different key).
	// The server's handler still has fix.raKey, so decryption MUST fail.
	bogusRAKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey bogus RA: %v", err)
	}
	bogusRACert := selfSignedRSACert(t, bogusRAKey, "ra-bogus-not-on-server")
	bogusFix := &chromeOSStackFixture{
		raKey:      bogusRAKey,
		raCert:     bogusRACert,
		deviceKey:  fix.deviceKey,
		deviceCert: fix.deviceCert,
	}
	pkiMessage := buildChromeOSStylePKIMessage(t, bogusFix, domain.SCEPMessageTypePKCSReq, "txn-ra-mismatch", "shared-secret-123", "ra-mismatch.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))

	w, _ := postPKIOperation(t, fix.handler, pkiMessage)
	// RFC 8894 path returns FAILURE+badMessageCheck CertRep (200), MVP
	// fall-through returns 400. Either is acceptable — what we MUST
	// see is "the issuer never received the CSR."
	if w.Code != http.StatusBadRequest && w.Code != http.StatusOK {
		t.Errorf("POST PKIOperation (RA-key mismatch): got %d, want 400 (MVP fall-through) or 200 (CertRep+failInfo)", w.Code)
	}
	if fix.svc.pkcsReqEnvelope != nil {
		t.Error("PKCSReqWithEnvelope was reached despite the RA-cert/key mismatch — decrypt-failure leaked through to the service")
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_3DESBackwardCompat — closure-bundle
// gap M-1 / acceptance D.2. RFC 8894 §3.5.2 names DES-EDE3-CBC
// (1.2.840.113549.3.7) as a "supported but discouraged" content-encryption
// algorithm for backward compat with older Cisco IOS / Apple legacy
// clients. Verify the parser accepts this OID + the handler reaches
// the service with a decoded CSR.
func TestSCEPHandler_ChromeOSPKIMessage_3DESBackwardCompat(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	tdesKey := aesKeyForOID(pkcs7.OIDDESEDE3CBC) // 24 bytes (3DES K1||K2||K3)

	csrDER := buildTestCSR(t, fix.deviceKey, "tdes.example.com", "shared-secret-123")

	iv := make([]byte, des.BlockSize) // 8 bytes for 3DES
	if _, err := rand.Read(iv); err != nil {
		t.Fatalf("rand iv: %v", err)
	}
	ciphertext := tripleDESCBCEncrypt(t, tdesKey, iv, csrDER)
	encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, fix.raCert.PublicKey.(*rsa.PublicKey), tdesKey)
	if err != nil {
		t.Fatalf("rsa encrypt 3des key: %v", err)
	}
	envelopedData := buildEnvelopedDataForTest(t, fix.raCert, encryptedKey, iv, ciphertext, pkcs7.OIDDESEDE3CBC)
	pkiMessage := buildSignedDataForTest(t, fix.deviceKey, fix.deviceCert, domain.SCEPMessageTypePKCSReq, "txn-3des", []byte("0123456789abcdef"), envelopedData)

	w, body := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusOK {
		t.Fatalf("POST PKIOperation (3DES legacy): got %d, want 200 (RFC 8894 §3.5.2 backward-compat) — body=%q", w.Code, body)
	}
	if fix.svc.pkcsReqEnvelope == nil {
		t.Fatal("PKCSReqWithEnvelope was NOT reached — 3DES decrypt path didn't make it to the service")
	}
}

// TestSCEPHandler_ChromeOSPKIMessage_RSACSR — closure-bundle gap M-1 /
// acceptance D.4. Pins the "RSA CSR" matrix corner explicitly so a
// future helper refactor that quietly drops the RSA path doesn't
// disappear from the test count without a counter dropping. The
// shared positive-flow assertions live in
// assertChromeOSPositiveCertRep so the matrix-pair {RSA, ECDSA} stays
// readable.
func TestSCEPHandler_ChromeOSPKIMessage_RSACSR(t *testing.T) {
	fix := newChromeOSStackFixture(t)
	pkiMessage := buildChromeOSStylePKIMessage(t, fix, domain.SCEPMessageTypePKCSReq, "txn-rsa-csr", "shared-secret-123", "rsa-csr.example.com", aesKeyForOID(pkcs7.OIDAES256CBC))
	assertChromeOSPositiveCertRep(t, fix, pkiMessage)
}

// TestSCEPHandler_ChromeOSPKIMessage_ECDSACSR — closure-bundle gap M-1
// / acceptance D.3. The CSR's keypair is ECDSA P-256; the device's
// transient signerInfo identity stays RSA (matches what real ChromeOS
// + Intune-managed devices commonly emit — device identity is a
// long-lived RSA key, the new cert can be ECDSA). Verifies the
// handler doesn't choke on the inner CSR's algorithm even when the
// outer SignerInfo is RSA-SHA256.
func TestSCEPHandler_ChromeOSPKIMessage_ECDSACSR(t *testing.T) {
	fix := newChromeOSStackFixture(t)

	csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("ecdsa.GenerateKey: %v", err)
	}
	csrDER := buildTestECDSACSR(t, csrKey, "ecdsa-csr.example.com", "shared-secret-123")

	symKey := aesKeyForOID(pkcs7.OIDAES256CBC)
	iv := make([]byte, aes.BlockSize)
	if _, err := rand.Read(iv); err != nil {
		t.Fatalf("rand iv: %v", err)
	}
	ciphertext := aesCBCEncrypt(t, symKey, iv, csrDER)
	encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, fix.raCert.PublicKey.(*rsa.PublicKey), symKey)
	if err != nil {
		t.Fatalf("rsa encrypt symKey: %v", err)
	}
	envelopedData := buildEnvelopedDataForTest(t, fix.raCert, encryptedKey, iv, ciphertext, pkcs7.OIDAES256CBC)
	pkiMessage := buildSignedDataForTest(t, fix.deviceKey, fix.deviceCert, domain.SCEPMessageTypePKCSReq, "txn-ecdsa-csr", []byte("0123456789abcdef"), envelopedData)
	assertChromeOSPositiveCertRep(t, fix, pkiMessage)
}

// assertChromeOSPositiveCertRep is the shared positive-flow assertion
// helper for the {RSA, ECDSA} CSR matrix tests. Asserts HTTP 200 +
// content-type + the service-level mock saw the envelope.
func assertChromeOSPositiveCertRep(t *testing.T, fix *chromeOSStackFixture, pkiMessage []byte) {
	t.Helper()
	w, body := postPKIOperation(t, fix.handler, pkiMessage)
	if w.Code != http.StatusOK {
		t.Fatalf("POST PKIOperation: got %d, want 200 (body=%q)", w.Code, body)
	}
	if got := w.Header().Get("Content-Type"); got != "application/x-pki-message" {
		t.Errorf("Content-Type = %q, want application/x-pki-message", got)
	}
	if fix.svc.pkcsReqEnvelope == nil {
		t.Fatal("PKCSReqWithEnvelope was NOT reached — handler dispatched to MVP path or rejected the message")
	}
}

// buildTestECDSACSR mirrors buildTestCSR but for an ECDSA P-256
// signing key. Closure-bundle Phase D helper. The CSR carries the
// challengePassword attribute the same way the RSA helper does.
func buildTestECDSACSR(t *testing.T, key *ecdsa.PrivateKey, commonName, challengePassword string) []byte {
	t.Helper()
	tmpl := &x509.CertificateRequest{
		Subject:         pkix.Name{CommonName: commonName},
		ExtraExtensions: []pkix.Extension{},
		Attributes: []pkix.AttributeTypeAndValueSET{
			{
				Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7},
				Value: [][]pkix.AttributeTypeAndValue{
					{{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7}, Value: challengePassword}},
				},
			},
		},
	}
	der, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
	if err != nil {
		t.Fatalf("CreateCertificateRequest (ECDSA): %v", err)
	}
	return der
}

// tripleDESCBCEncrypt mirrors aesCBCEncrypt for 3DES — used by the
// 3DES backward-compat test. PKCS#7 padding to 8-byte blocks.
func tripleDESCBCEncrypt(t *testing.T, key, iv, plaintext []byte) []byte {
	t.Helper()
	block, err := des.NewTripleDESCipher(key) //nolint:gosec // RFC 8894 §3.5.2 legacy backward-compat test fixture
	if err != nil {
		t.Fatalf("des.NewTripleDESCipher: %v", err)
	}
	bs := block.BlockSize()
	padLen := bs - len(plaintext)%bs
	padded := append([]byte{}, plaintext...)
	for i := 0; i < padLen; i++ {
		padded = append(padded, byte(padLen))
	}
	enc := cipher.NewCBCEncrypter(block, iv)
	out := make([]byte, len(padded))
	enc.CryptBlocks(out, padded)
	return out
}

// TestSCEPHandler_MVPCompat_StillWorks asserts the existing MVP path (raw
// CSR inside a stripped SignedData, no EnvelopedData) STILL works for
// backward compat with lightweight clients.
func TestSCEPHandler_MVPCompat_StillWorks(t *testing.T) {
	// Build an MVP-shape request: a SignedData whose encapContent is a
	// raw CSR (no EnvelopedData wrapper). The legacy handler path
	// extractCSRFromPKCS7 unwraps it.
	deviceKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa.GenerateKey: %v", err)
	}
	csrDER := buildTestCSR(t, deviceKey, "mvp.example.com", "mvp-shared-secret")

	// Wrap in MVP-shape PKCS#7 SignedData (encapContent = CSR DER as
	// OCTET STRING). The existing extractCSRFromPKCS7 handles this.
	mvpPKCS7 := buildMVPSignedData(t, csrDER)

	svc := &chromeOSMockSCEPService{
		enrollResult: &domain.SCEPEnrollResult{
			CertPEM: pemEncodeCert(selfSignedRSACertRaw(t, deviceKey, "mvp-issued.example.com")),
		},
	}
	// Note: NO RA pair set — the handler runs MVP-only.
	handler := NewSCEPHandler(svc)
	w, body := postPKIOperation(t, handler, mvpPKCS7)
	if w.Code != http.StatusOK {
		t.Fatalf("MVP path POST: got %d, want 200 (body=%q)", w.Code, body)
	}
	// Response is the legacy certs-only PKCS#7, NOT a CertRep PKIMessage.
	if got := w.Header().Get("Content-Type"); got != "application/x-pki-message" {
		t.Errorf("Content-Type = %q, want application/x-pki-message", got)
	}
}

// --- helpers -------------------------------------------------------------

func postPKIOperation(t *testing.T, h SCEPHandler, body []byte) (*httptest.ResponseRecorder, []byte) {
	t.Helper()
	req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", bytes.NewReader(body))
	w := httptest.NewRecorder()
	h.HandleSCEP(w, req)
	respBody, _ := io.ReadAll(w.Body)
	return w, respBody
}

// buildChromeOSStylePKIMessage builds a real SCEP PKIMessage targeting the
// fixture's RA cert. Mirrors what ChromeOS / micromdm-style clients emit:
// SignedData(SignerInfo(deviceCert, sig over auth-attrs)) wrapping an
// EnvelopedData(KTRI(raCert), AES-CBC(CSR + challengePassword)).
func buildChromeOSStylePKIMessage(t *testing.T, fix *chromeOSStackFixture, messageType domain.SCEPMessageType, transactionID, challengePassword, csrCN string, symKey []byte) []byte {
	t.Helper()

	// 1. Build the inner CSR carrying the challengePassword attribute.
	csrDER := buildTestCSR(t, fix.deviceKey, csrCN, challengePassword)

	// 2. Encrypt the CSR via AES-CBC under symKey + random IV.
	iv := make([]byte, aes.BlockSize)
	if _, err := rand.Read(iv); err != nil {
		t.Fatalf("rand iv: %v", err)
	}
	ciphertext := aesCBCEncrypt(t, symKey, iv, csrDER)

	// 3. RSA-encrypt the symKey to fix.raCert.PublicKey.
	encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, fix.raCert.PublicKey.(*rsa.PublicKey), symKey)
	if err != nil {
		t.Fatalf("rsa encrypt symKey: %v", err)
	}

	// 4. Build EnvelopedData wrapping ciphertext.
	envelopedData := buildEnvelopedDataForTest(t, fix.raCert, encryptedKey, iv, ciphertext, oidForAESKeyLen(t, len(symKey)))

	// 5. Build the SignedData carrying the EnvelopedData with a
	//    signerInfo signed by the device's transient cert/key.
	signedData := buildSignedDataForTest(t, fix.deviceKey, fix.deviceCert, messageType, transactionID, []byte("0123456789abcdef"), envelopedData)
	return signedData
}

// withContentEncryptionOID rewrites the AES OID inside an already-built
// PKIMessage by re-building from scratch with the new OID. Simpler than
// surgically patching the bytes.
func withContentEncryptionOID(t *testing.T, _ []byte, fix *chromeOSStackFixture, oid asn1.ObjectIdentifier, symKey []byte) []byte {
	t.Helper()
	csrDER := buildTestCSR(t, fix.deviceKey, "aes.example.com", "shared-secret-123")
	iv := make([]byte, 16)
	if _, err := rand.Read(iv); err != nil {
		t.Fatalf("rand iv: %v", err)
	}
	ciphertext := aesCBCEncrypt(t, symKey, iv, csrDER)
	encryptedKey, err := rsa.EncryptPKCS1v15(rand.Reader, fix.raCert.PublicKey.(*rsa.PublicKey), symKey)
	if err != nil {
		t.Fatalf("rsa encrypt: %v", err)
	}
	envelopedData := buildEnvelopedDataForTest(t, fix.raCert, encryptedKey, iv, ciphertext, oid)
	return buildSignedDataForTest(t, fix.deviceKey, fix.deviceCert, domain.SCEPMessageTypePKCSReq, "txn-aes", []byte("0123456789abcdef"), envelopedData)
}

func aesCBCEncrypt(t *testing.T, key, iv, plaintext []byte) []byte {
	t.Helper()
	block, err := aes.NewCipher(key)
	if err != nil {
		t.Fatalf("aes.NewCipher: %v", err)
	}
	bs := block.BlockSize()
	padLen := bs - len(plaintext)%bs
	padded := append([]byte{}, plaintext...)
	for i := 0; i < padLen; i++ {
		padded = append(padded, byte(padLen))
	}
	enc := cipher.NewCBCEncrypter(block, iv)
	out := make([]byte, len(padded))
	enc.CryptBlocks(out, padded)
	return out
}

// oidForAESKeyLen maps an AES key length to its CBC OID. Helper for the
// AES-variants table-driven test.
func oidForAESKeyLen(t *testing.T, n int) asn1.ObjectIdentifier {
	t.Helper()
	switch n {
	case 16:
		return pkcs7.OIDAES128CBC
	case 24:
		return pkcs7.OIDAES192CBC
	case 32:
		return pkcs7.OIDAES256CBC
	}
	t.Fatalf("oidForAESKeyLen: unsupported key length %d", n)
	return nil
}

// aesKeyForOID returns a deterministic-length symmetric key matching the
// AES variant identified by oid. Test-only — production uses crypto/rand.
func aesKeyForOID(oid asn1.ObjectIdentifier) []byte {
	switch {
	case oid.Equal(pkcs7.OIDAES128CBC):
		return bytes.Repeat([]byte{0x42}, 16)
	case oid.Equal(pkcs7.OIDAES192CBC):
		return bytes.Repeat([]byte{0x42}, 24)
	case oid.Equal(pkcs7.OIDAES256CBC):
		return bytes.Repeat([]byte{0x42}, 32)
	case oid.Equal(pkcs7.OIDDESEDE3CBC):
		return bytes.Repeat([]byte{0x42}, 24)
	}
	return nil
}

// buildTestCSR creates a CSR with a challengePassword attribute. Used by
// the buildChromeOSStylePKIMessage helper to populate the EnvelopedData
// inner content.
func buildTestCSR(t *testing.T, key *rsa.PrivateKey, commonName, challengePassword string) []byte {
	t.Helper()
	// Build the challengePassword attribute (RFC 2985 §5.4.1, OID
	// 1.2.840.113549.1.9.7).
	cpAttr := pkix.AttributeTypeAndValue{
		Type:  asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7},
		Value: challengePassword,
	}
	cpAttrSet, err := asn1.Marshal(cpAttr)
	if err != nil {
		t.Fatalf("marshal cp attr: %v", err)
	}
	tmpl := &x509.CertificateRequest{
		Subject: pkix.Name{CommonName: commonName},
		// Inject the challengePassword as a raw extra extension via the
		// CSR Attributes field.
		ExtraExtensions: []pkix.Extension{},
		Attributes: []pkix.AttributeTypeAndValueSET{
			{
				Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7},
				Value: [][]pkix.AttributeTypeAndValue{
					{{Type: asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 7}, Value: challengePassword}},
				},
			},
		},
	}
	_ = cpAttrSet
	der, err := x509.CreateCertificateRequest(rand.Reader, tmpl, key)
	if err != nil {
		t.Fatalf("CreateCertificateRequest: %v", err)
	}
	return der
}

// buildEnvelopedDataForTest builds an EnvelopedData targeting raCert with
// a single KTRI carrying the encrypted symmetric key + the AES-CBC
// ciphertext. Mirrors the Phase 3 buildEnvelopedDataAES256 internal helper
// but exposed at test scope.
func buildEnvelopedDataForTest(t *testing.T, raCert *x509.Certificate, encryptedKey, iv, ciphertext []byte, contentEncOID asn1.ObjectIdentifier) []byte {
	t.Helper()
	// IssuerAndSerial of the recipient.
	serialDER, err := asn1.Marshal(raCert.SerialNumber)
	if err != nil {
		t.Fatalf("marshal serial: %v", err)
	}
	risBody := append([]byte{}, raCert.RawIssuer...)
	risBody = append(risBody, serialDER...)
	risBytes := pkcs7.ASN1Wrap(0x30, risBody)

	keyEncAlg := pkix.AlgorithmIdentifier{Algorithm: pkcs7.OIDRSAEncryption, Parameters: asn1.NullRawValue}
	keyEncAlgBytes, err := asn1.Marshal(keyEncAlg)
	if err != nil {
		t.Fatalf("marshal keyEncAlg: %v", err)
	}
	encryptedKeyBytes := pkcs7.ASN1Wrap(0x04, encryptedKey)

	ktriBody := append([]byte{}, []byte{0x02, 0x01, 0x00}...)
	ktriBody = append(ktriBody, risBytes...)
	ktriBody = append(ktriBody, keyEncAlgBytes...)
	ktriBody = append(ktriBody, encryptedKeyBytes...)
	ktriBytes := pkcs7.ASN1Wrap(0x30, ktriBody)

	recipientInfosBytes := pkcs7.ASN1Wrap(0x31, ktriBytes)

	ivOctet := pkcs7.ASN1Wrap(0x04, iv)
	contentAlg := pkix.AlgorithmIdentifier{
		Algorithm:  contentEncOID,
		Parameters: asn1.RawValue{FullBytes: ivOctet},
	}
	contentAlgBytes, err := asn1.Marshal(contentAlg)
	if err != nil {
		t.Fatalf("marshal contentAlg: %v", err)
	}

	encContentField := pkcs7.ASN1Wrap(0x80, ciphertext)
	oidDataBytes := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}
	eciBody := append([]byte{}, oidDataBytes...)
	eciBody = append(eciBody, contentAlgBytes...)
	eciBody = append(eciBody, encContentField...)
	eciBytes := pkcs7.ASN1Wrap(0x30, eciBody)

	envBody := append([]byte{}, []byte{0x02, 0x01, 0x00}...)
	envBody = append(envBody, recipientInfosBytes...)
	envBody = append(envBody, eciBytes...)
	return pkcs7.ASN1Wrap(0x30, envBody)
}

// buildSignedDataForTest builds a CMS SignedData with the device cert as
// the signer + auth-attrs carrying SCEP messageType / transactionID /
// senderNonce + messageDigest of the encapContent.
func buildSignedDataForTest(t *testing.T, signerKey *rsa.PrivateKey, signerCert *x509.Certificate, messageType domain.SCEPMessageType, transactionID string, senderNonce, encapContent []byte) []byte {
	t.Helper()
	contentDigest := sha256.Sum256(encapContent)

	// Auth-attrs SET-OF body.
	var attrSetBody []byte
	attrSetBody = append(attrSetBody, attrSeqHelper(t, pkcs7.OIDContentType, pkcs7.ASN1Wrap(0x06, []byte{0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}))...)
	attrSetBody = append(attrSetBody, attrSeqHelper(t, pkcs7.OIDMessageDigest, pkcs7.ASN1Wrap(0x04, contentDigest[:]))...)
	attrSetBody = append(attrSetBody, attrSeqHelper(t, pkcs7.OIDSCEPMessageType, pkcs7.ASN1Wrap(0x13, []byte(intToASCII(int(messageType)))))...)
	attrSetBody = append(attrSetBody, attrSeqHelper(t, pkcs7.OIDSCEPTransactionID, pkcs7.ASN1Wrap(0x13, []byte(transactionID)))...)
	attrSetBody = append(attrSetBody, attrSeqHelper(t, pkcs7.OIDSCEPSenderNonce, pkcs7.ASN1Wrap(0x04, senderNonce))...)

	// Sign over SET OF Attribute (RFC 5652 §5.4 quirk).
	signedAttrsForSig := pkcs7.ASN1Wrap(0x31, attrSetBody)
	digest := sha256.Sum256(signedAttrsForSig)
	sig, err := rsa.SignPKCS1v15(rand.Reader, signerKey, 5, digest[:]) // 5 = crypto.SHA256
	if err != nil {
		t.Fatalf("sign: %v", err)
	}

	// SignerInfo SEQUENCE.
	versionBytes := []byte{0x02, 0x01, 0x01}
	serialDER, _ := asn1.Marshal(signerCert.SerialNumber)
	sidBody := append([]byte{}, signerCert.RawIssuer...)
	sidBody = append(sidBody, serialDER...)
	sidBytes := pkcs7.ASN1Wrap(0x30, sidBody)

	digestAlg := pkix.AlgorithmIdentifier{Algorithm: pkcs7.OIDSHA256, Parameters: asn1.NullRawValue}
	digestAlgBytes, _ := asn1.Marshal(digestAlg)

	signedAttrsImplicit := pkcs7.ASN1Wrap(0xa0, attrSetBody)

	sigAlg := pkix.AlgorithmIdentifier{Algorithm: pkcs7.OIDRSAWithSHA256, Parameters: asn1.NullRawValue}
	sigAlgBytes, _ := asn1.Marshal(sigAlg)

	sigOctet := pkcs7.ASN1Wrap(0x04, sig)

	siBody := append([]byte{}, versionBytes...)
	siBody = append(siBody, sidBytes...)
	siBody = append(siBody, digestAlgBytes...)
	siBody = append(siBody, signedAttrsImplicit...)
	siBody = append(siBody, sigAlgBytes...)
	siBody = append(siBody, sigOctet...)
	siBytes := pkcs7.ASN1Wrap(0x30, siBody)

	// encapContentInfo
	octetWrap := pkcs7.ASN1Wrap(0x04, encapContent)
	explicitWrap := pkcs7.ASN1Wrap(0xa0, octetWrap)
	oidDataBytes := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}
	encapBody := append([]byte{}, oidDataBytes...)
	encapBody = append(encapBody, explicitWrap...)
	encapBytes := pkcs7.ASN1Wrap(0x30, encapBody)

	// certificates [0] IMPLICIT SET OF Certificate
	certsBytes := pkcs7.ASN1Wrap(0xa0, signerCert.Raw)

	// digestAlgorithms SET OF
	digestAlgsBytes := pkcs7.ASN1Wrap(0x31, digestAlgBytes)
	// signerInfos SET OF
	signerInfosBytes := pkcs7.ASN1Wrap(0x31, siBytes)

	// SignedData SEQUENCE
	sdBody := append([]byte{}, []byte{0x02, 0x01, 0x01}...)
	sdBody = append(sdBody, digestAlgsBytes...)
	sdBody = append(sdBody, encapBytes...)
	sdBody = append(sdBody, certsBytes...)
	sdBody = append(sdBody, signerInfosBytes...)
	sdSeq := pkcs7.ASN1Wrap(0x30, sdBody)

	// ContentInfo wrap
	contentField := pkcs7.ASN1Wrap(0xa0, sdSeq)
	oidSignedData := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02}
	ciBody := append([]byte{}, oidSignedData...)
	ciBody = append(ciBody, contentField...)
	return pkcs7.ASN1Wrap(0x30, ciBody)
}

// buildMVPSignedData builds a degenerate SignedData where the encapContent
// is the raw CSR bytes — what lightweight SCEP clients send. Used by the
// MVP-compat test to confirm the legacy parser still works.
func buildMVPSignedData(t *testing.T, csrDER []byte) []byte {
	t.Helper()
	octetWrap := pkcs7.ASN1Wrap(0x04, csrDER)
	explicitWrap := pkcs7.ASN1Wrap(0xa0, octetWrap)
	oidDataBytes := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01}
	encapBody := append([]byte{}, oidDataBytes...)
	encapBody = append(encapBody, explicitWrap...)
	encapBytes := pkcs7.ASN1Wrap(0x30, encapBody)

	digestAlgsBytes := pkcs7.ASN1Wrap(0x31, nil)
	signerInfosBytes := pkcs7.ASN1Wrap(0x31, nil)

	sdBody := append([]byte{}, []byte{0x02, 0x01, 0x01}...)
	sdBody = append(sdBody, digestAlgsBytes...)
	sdBody = append(sdBody, encapBytes...)
	sdBody = append(sdBody, signerInfosBytes...)
	sdSeq := pkcs7.ASN1Wrap(0x30, sdBody)

	contentField := pkcs7.ASN1Wrap(0xa0, sdSeq)
	oidSignedData := []byte{0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x02}
	ciBody := append([]byte{}, oidSignedData...)
	ciBody = append(ciBody, contentField...)
	return pkcs7.ASN1Wrap(0x30, ciBody)
}

func attrSeqHelper(t *testing.T, oid asn1.ObjectIdentifier, value []byte) []byte {
	t.Helper()
	oidBytes, err := asn1.Marshal(oid)
	if err != nil {
		t.Fatalf("marshal OID %v: %v", oid, err)
	}
	setOfValue := pkcs7.ASN1Wrap(0x31, value)
	body := append([]byte{}, oidBytes...)
	body = append(body, setOfValue...)
	return pkcs7.ASN1Wrap(0x30, body)
}

func decodeFirstSetMember(t *testing.T, rv asn1.RawValue) string {
	t.Helper()
	var inner asn1.RawValue
	if _, err := asn1.Unmarshal(rv.Bytes, &inner); err != nil {
		t.Fatalf("unmarshal SET first member: %v", err)
	}
	return string(inner.Bytes)
}

func intToASCII(i int) string {
	if i == 0 {
		return "0"
	}
	var b []byte
	for i > 0 {
		b = append([]byte{byte('0' + i%10)}, b...)
		i /= 10
	}
	return string(b)
}

func selfSignedRSACert(t *testing.T, key *rsa.PrivateKey, cn string) *x509.Certificate {
	t.Helper()
	der := selfSignedRSACertRaw(t, key, cn)
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		t.Fatalf("ParseCertificate: %v", err)
	}
	return cert
}

func selfSignedRSACertRaw(t *testing.T, key *rsa.PrivateKey, cn string) []byte {
	t.Helper()
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(time.Now().UnixNano()),
		Subject:      pkix.Name{CommonName: cn},
		Issuer:       pkix.Name{CommonName: cn},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(30 * 24 * time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		t.Fatalf("CreateCertificate: %v", err)
	}
	return der
}

func pemEncodeCert(der []byte) string {
	return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
}

// silence unused-import warnings — these packages are referenced inside
// helpers above; Go's import-pruning is conservative around test-only
// uses through other test files.
var (
	_ = ecdsa.PublicKey{}
	_ = elliptic.P256
	_ = des.NewTripleDESCipher
)
