package pkcs7

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"math/big"
	"testing"
	"time"

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

// FuzzBuildCertRepPKIMessage stresses the CertRep builder with attacker-
// controlled transactionID + nonce + signerCert bytes. The invariants are:
//   1. No panic for arbitrary inputs.
//   2. When build succeeds AND status is success, the output parses back
//      via ParseSignedData (round-trip soundness — the prompt's required
//      fuzz invariant).
//
// SCEP RFC 8894 + Intune master bundle Phase 3.3.
//
// The fuzzer holds the RA pair constant (one-time setup) and lets the
// fuzz engine vary the unstable inputs. Errors from BuildCertRepPKIMessage
// are expected for malformed signerCert bytes; only a panic = bug.

func FuzzBuildCertRepPKIMessage(f *testing.F) {
	// Seed: empty everything (should error cleanly via the nil-args gate).
	f.Add("", []byte{}, []byte{})
	// Seed: minimal inputs that exercise the failure-path code (no
	// SignerCert needed because Status=Failure short-circuits the
	// EnvelopedData build).
	f.Add("txn-1", make([]byte, 16), []byte{})

	// One-time setup: RA pair stays constant across fuzz iterations.
	raKey, raCert := genTestRSARAFuzz()
	if raKey == nil {
		f.Skip("test RA pair generation failed; environment lacks crypto/rand?")
	}

	f.Fuzz(func(t *testing.T, transactionID string, senderNonce []byte, signerCert []byte) {
		req := &domain.SCEPRequestEnvelope{
			MessageType:   domain.SCEPMessageTypePKCSReq,
			TransactionID: transactionID,
			SenderNonce:   senderNonce,
			SignerCert:    signerCert,
		}
		// Failure path: never needs SignerCert. No panic, no requirement
		// on output (the failure shape is correct by construction).
		respFail := &domain.SCEPResponseEnvelope{
			Status:         domain.SCEPStatusFailure,
			FailInfo:       domain.SCEPFailBadRequest,
			TransactionID:  transactionID,
			RecipientNonce: senderNonce,
		}
		_, _ = BuildCertRepPKIMessage(req, respFail, raCert, raKey)

		// Success path with arbitrary signerCert bytes: most inputs will
		// fail to parse as a real cert; that's fine, BuildCertRep returns
		// an error rather than panicking. When build succeeds (rare for
		// random bytes), assert the output parses back.
		respSuccess := &domain.SCEPResponseEnvelope{
			Status:         domain.SCEPStatusSuccess,
			TransactionID:  transactionID,
			RecipientNonce: senderNonce,
			Result: &domain.SCEPEnrollResult{
				CertPEM: minimalIssuedCertPEMFuzz(raKey),
			},
		}
		out, err := BuildCertRepPKIMessage(req, respSuccess, raCert, raKey)
		if err != nil {
			return // expected for arbitrary signerCert; no panic = ok
		}
		// Build succeeded — verify round-trip soundness.
		sd, err := ParseSignedData(out)
		if err != nil {
			t.Errorf("BuildCertRepPKIMessage produced output that fails ParseSignedData: %v", err)
			return
		}
		if len(sd.SignerInfos) == 0 {
			t.Errorf("BuildCertRepPKIMessage produced output with no signerInfos")
		}
	})
}

// genTestRSARAFuzz materialises a one-time RA pair for the fuzz seed
// setup. Mirrors genTestRSARA from the round-trip tests but doesn't
// take *testing.T (called from f.Fuzz setup, not a test body).
func genTestRSARAFuzz() (*rsa.PrivateKey, *x509.Certificate) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, nil
	}
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "fuzz-ra"},
		Issuer:       pkix.Name{CommonName: "fuzz-ra"},
		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 {
		return nil, nil
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		return nil, nil
	}
	return key, cert
}

// minimalIssuedCertPEMFuzz returns a tiny self-signed PEM cert reusing
// the RA key. Avoids per-fuzz-iter rsa.GenerateKey overhead (which would
// dominate the fuzz throughput).
func minimalIssuedCertPEMFuzz(key *rsa.PrivateKey) string {
	// We construct on demand since the issued cert template doesn't
	// matter beyond being a parseable PEM-wrapped DER cert.
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(2),
		Subject:      pkix.Name{CommonName: "fuzz-issued"},
		Issuer:       pkix.Name{CommonName: "fuzz-issued"},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		return ""
	}
	return "-----BEGIN CERTIFICATE-----\n" +
		derToBase64Fuzz(der) +
		"-----END CERTIFICATE-----\n"
}

func derToBase64Fuzz(der []byte) string {
	const enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
	var out []byte
	pad := (3 - len(der)%3) % 3
	padded := append(append([]byte{}, der...), make([]byte, pad)...)
	for i := 0; i < len(padded); i += 3 {
		v := uint32(padded[i])<<16 | uint32(padded[i+1])<<8 | uint32(padded[i+2])
		out = append(out, enc[v>>18&0x3f], enc[v>>12&0x3f], enc[v>>6&0x3f], enc[v&0x3f])
	}
	for i := 0; i < pad; i++ {
		out[len(out)-1-i] = '='
	}
	// Wrap at 64 chars per PEM convention.
	var wrapped []byte
	for i := 0; i < len(out); i += 64 {
		end := i + 64
		if end > len(out) {
			end = len(out)
		}
		wrapped = append(wrapped, out[i:end]...)
		wrapped = append(wrapped, '\n')
	}
	return string(wrapped)
}
