//go:build integration

package acme_test // external test package — tests the public surface

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"crypto/x509"
	"encoding/asn1"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"os"
	"strings"
	"testing"
)

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

func skipIfNotLive(t *testing.T) {
	t.Helper()
	if os.Getenv("METACRYPT_LIVE_TEST") != "1" {
		t.Skip("set METACRYPT_LIVE_TEST=1 to run live integration tests")
	}
}

func liveAddr() string  { return envOr("METACRYPT_ADDR", "https://127.0.0.1:18443") }
func liveMount() string { return envOr("METACRYPT_ACME_MOUNT", "pki") }
func liveToken() string { return os.Getenv("METACRYPT_TOKEN") }

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// liveHTTPClient returns an *http.Client configured with the CA cert from
// METACRYPT_CA_CERT, falling back to InsecureSkipVerify if unset.
func liveHTTPClient(t *testing.T) *http.Client {
	t.Helper()

	tlsCfg := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}

	caPath := os.Getenv("METACRYPT_CA_CERT")
	if caPath != "" {
		caPEM, err := os.ReadFile(caPath)
		if err != nil {
			t.Fatalf("read CA cert %s: %v", caPath, err)
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(caPEM) {
			t.Fatalf("failed to parse CA cert from %s", caPath)
		}
		tlsCfg.RootCAs = pool
	} else {
		tlsCfg.InsecureSkipVerify = true //nolint:gosec
	}

	return &http.Client{
		Transport: &http.Transport{TLSClientConfig: tlsCfg},
	}
}

// ---------------------------------------------------------------------------
// liveTestEnv — shared per-test state with lazy account creation
// ---------------------------------------------------------------------------

type liveTestEnv struct {
	t      *testing.T
	client *http.Client
	addr   string
	mount  string
	token  string

	// directory is fetched once to discover the server's base URL.
	directory     map[string]interface{}
	dirFetched    bool
	acmeBaseURL   string // our reachable base URL for HTTP requests
	serverBaseURL string // the server's own base URL, for JWS URL fields

	// lazily initialized
	accountURL string // full URL returned in Location header
	accountID  string
	key        *ecdsa.PrivateKey
	jwk        json.RawMessage
}

func newLiveEnv(t *testing.T) *liveTestEnv {
	t.Helper()
	return &liveTestEnv{
		t:      t,
		client: liveHTTPClient(t),
		addr:   liveAddr(),
		mount:  liveMount(),
		token:  liveToken(),
	}
}

// acmeURL returns the full URL for an ACME path (e.g. "/directory").
// It uses the server's own base URL discovered from the directory.
func (e *liveTestEnv) acmeURL(path string) string {
	e.t.Helper()
	e.ensureDirectory()
	return e.acmeBaseURL + path
}

// rawACMEURL returns a URL without consulting the directory — for the
// initial directory fetch itself.
func (e *liveTestEnv) rawACMEURL(path string) string {
	return e.addr + "/acme/" + e.mount + path
}

// jwsURL returns the URL the server expects in JWS URL fields. This may
// differ from acmeURL when the server's ExternalURL is not configured.
func (e *liveTestEnv) jwsURL(path string) string {
	e.t.Helper()
	e.ensureDirectory()
	return e.serverBaseURL + path
}

// mgmtURL returns the full URL for a management API path.
func (e *liveTestEnv) mgmtURL(path string) string {
	return e.addr + "/v1/acme/" + e.mount + path
}

// ensureDirectory fetches and caches the ACME directory.
func (e *liveTestEnv) ensureDirectory() {
	e.t.Helper()
	if e.dirFetched {
		return
	}

	resp, err := e.client.Get(e.rawACMEURL("/directory"))
	if err != nil {
		e.t.Fatalf("GET directory: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		e.t.Fatalf("GET directory: status %d: %s", resp.StatusCode, body)
	}

	var dir map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&dir); err != nil {
		e.t.Fatalf("decode directory: %v", err)
	}
	e.directory = dir
	e.dirFetched = true

	// Our reachable base URL for HTTP requests.
	e.acmeBaseURL = e.addr + "/acme/" + e.mount

	// The server's own base URL, used for JWS URL fields which must match
	// what the server expects. May differ from our reachable URL when the
	// server's ExternalURL is misconfigured.
	newNonce, ok := dir["newNonce"].(string)
	if !ok || newNonce == "" {
		e.t.Fatalf("directory missing newNonce")
	}
	e.serverBaseURL = strings.TrimSuffix(newNonce, "/new-nonce")
}

// getNonce fetches a fresh replay nonce from the server.
func (e *liveTestEnv) getNonce() string {
	e.t.Helper()
	e.ensureDirectory()

	req, err := http.NewRequest(http.MethodHead, e.acmeURL("/new-nonce"), nil)
	if err != nil {
		e.t.Fatalf("build nonce request: %v", err)
	}
	resp, err := e.client.Do(req)
	if err != nil {
		e.t.Fatalf("HEAD new-nonce: %v", err)
	}
	defer resp.Body.Close()

	nonce := resp.Header.Get("Replay-Nonce")
	if nonce == "" {
		e.t.Fatal("Replay-Nonce header is empty")
	}
	return nonce
}

// createEAB calls the management API to create an EAB credential.
// Returns kid and hmacKey (raw bytes).
func (e *liveTestEnv) createEAB() (kid string, hmacKey []byte) {
	e.t.Helper()
	if e.token == "" {
		e.t.Fatal("METACRYPT_TOKEN is required for EAB creation")
	}

	req, err := http.NewRequest(http.MethodPost, e.mgmtURL("/eab"), nil)
	if err != nil {
		e.t.Fatalf("build EAB request: %v", err)
	}
	req.Header.Set("Authorization", "Bearer "+e.token)

	resp, err := e.client.Do(req)
	if err != nil {
		e.t.Fatalf("POST eab: %v", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	if resp.StatusCode != http.StatusCreated {
		e.t.Fatalf("POST eab: status %d: %s", resp.StatusCode, body)
	}

	var eabResp struct {
		KID     string `json:"kid"`
		HMACKey []byte `json:"hmac_key"` // Go json.Marshal of []byte produces base64
	}
	if err := json.Unmarshal(body, &eabResp); err != nil {
		e.t.Fatalf("decode EAB response: %v (body: %s)", err, body)
	}
	if eabResp.KID == "" {
		e.t.Fatalf("EAB response missing kid (body: %s)", body)
	}
	if len(eabResp.HMACKey) == 0 {
		e.t.Fatalf("EAB response missing hmac_key (body: %s)", body)
	}
	return eabResp.KID, eabResp.HMACKey
}

// generateKey creates an ECDSA P-256 key pair and a JWK representation.
func (e *liveTestEnv) generateKey() (*ecdsa.PrivateKey, json.RawMessage) {
	e.t.Helper()
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		e.t.Fatalf("generate ES256 key: %v", err)
	}
	byteLen := (key.Curve.Params().BitSize + 7) / 8
	xBytes := key.PublicKey.X.Bytes()
	yBytes := key.PublicKey.Y.Bytes()
	for len(xBytes) < byteLen {
		xBytes = append([]byte{0}, xBytes...)
	}
	for len(yBytes) < byteLen {
		yBytes = append([]byte{0}, yBytes...)
	}
	jwk, err := json.Marshal(map[string]string{
		"kty": "EC",
		"crv": "P-256",
		"x":   base64.RawURLEncoding.EncodeToString(xBytes),
		"y":   base64.RawURLEncoding.EncodeToString(yBytes),
	})
	if err != nil {
		e.t.Fatalf("marshal JWK: %v", err)
	}
	return key, json.RawMessage(jwk)
}

// ensureAccount creates an ACME account if not already created.
func (e *liveTestEnv) ensureAccount() {
	e.t.Helper()
	if e.accountURL != "" {
		return
	}

	key, jwk := e.generateKey()
	e.key = key
	e.jwk = jwk

	kid, hmacKey := e.createEAB()

	nonce := e.getNonce()

	// JWS URL field must match the server's own base URL.
	jwsAccountURL := e.jwsURL("/new-account")
	eabJWS := liveSignEAB(e.t, kid, hmacKey, jwk, jwsAccountURL)

	// Build the new-account payload.
	payload, err := json.Marshal(map[string]interface{}{
		"termsOfServiceAgreed":   true,
		"contact":               []string{"mailto:live-test@metacircular.net"},
		"externalAccountBinding": json.RawMessage(eabJWS),
	})
	if err != nil {
		e.t.Fatalf("marshal new-account payload: %v", err)
	}

	// Build the outer JWS with embedded JWK (new-account uses JWK, not KID).
	header := map[string]interface{}{
		"alg":   "ES256",
		"nonce": nonce,
		"url":   jwsAccountURL,
		"jwk":   json.RawMessage(jwk),
	}
	body := liveSignJWS(e.t, key, "ES256", header, payload)

	// POST to our reachable URL (may differ from JWS URL).
	resp, err := e.client.Post(e.acmeURL("/new-account"), "application/jose+json", strings.NewReader(string(body)))
	if err != nil {
		e.t.Fatalf("POST new-account: %v", err)
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)

	if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
		e.t.Fatalf("POST new-account: status %d: %s", resp.StatusCode, respBody)
	}

	location := resp.Header.Get("Location")
	if location == "" {
		e.t.Fatal("new-account response missing Location header")
	}
	e.accountURL = location

	// Extract account ID from the Location URL (last path segment after /account/).
	if idx := strings.LastIndex(location, "/account/"); idx >= 0 {
		e.accountID = location[idx+len("/account/"):]
	} else {
		// Use the full URL as the ID / KID.
		e.accountID = location
	}
}

// postAsGet sends a POST-as-GET request (empty payload, KID auth).
// httpURL is where to send the request; jwsURL is the URL in the JWS header.
func (e *liveTestEnv) postAsGet(httpURL, jwsURL string) (*http.Response, []byte) {
	e.t.Helper()
	e.ensureAccount()

	nonce := e.getNonce()
	header := map[string]interface{}{
		"alg":   "ES256",
		"nonce": nonce,
		"url":   jwsURL,
		"kid":   e.accountURL,
	}
	body := liveSignJWS(e.t, e.key, "ES256", header, nil) // nil payload = POST-as-GET

	resp, err := e.client.Post(httpURL, "application/jose+json", strings.NewReader(string(body)))
	if err != nil {
		e.t.Fatalf("POST-as-GET %s: %v", httpURL, err)
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)
	return resp, respBody
}

// postSigned sends a signed POST with a payload (KID auth).
// httpURL is where to send the request; jwsURL is the URL in the JWS header.
func (e *liveTestEnv) postSigned(httpURL, jwsURL string, payload []byte) (*http.Response, []byte) {
	e.t.Helper()
	e.ensureAccount()

	nonce := e.getNonce()
	header := map[string]interface{}{
		"alg":   "ES256",
		"nonce": nonce,
		"url":   jwsURL,
		"kid":   e.accountURL,
	}
	body := liveSignJWS(e.t, e.key, "ES256", header, payload)

	resp, err := e.client.Post(httpURL, "application/jose+json", strings.NewReader(string(body)))
	if err != nil {
		e.t.Fatalf("POST %s: %v", httpURL, err)
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)
	return resp, respBody
}

// ---------------------------------------------------------------------------
// JWS signing helpers (local implementations for external test package)
// ---------------------------------------------------------------------------

// ecdsaSigASN1 is used to decode an ASN.1 ECDSA signature into R and S.
type ecdsaSigASN1 struct {
	R *big.Int
	S *big.Int
}

// liveSignJWS creates a flattened JWS. header is marshaled as the protected
// header. If payload is nil, the payload field is empty (POST-as-GET).
func liveSignJWS(t *testing.T, key *ecdsa.PrivateKey, alg string, header map[string]interface{}, payload []byte) []byte {
	t.Helper()

	headerJSON, err := json.Marshal(header)
	if err != nil {
		t.Fatalf("marshal JWS header: %v", err)
	}
	protected := base64.RawURLEncoding.EncodeToString(headerJSON)

	var encodedPayload string
	if payload != nil {
		encodedPayload = base64.RawURLEncoding.EncodeToString(payload)
	}

	signingInput := []byte(protected + "." + encodedPayload)

	var sig []byte
	switch alg {
	case "ES256":
		digest := sha256.Sum256(signingInput)
		derSig, err := ecdsa.SignASN1(rand.Reader, key, digest[:])
		if err != nil {
			t.Fatalf("sign ES256: %v", err)
		}
		var parsed ecdsaSigASN1
		if _, err := asn1.Unmarshal(derSig, &parsed); err != nil {
			t.Fatalf("unmarshal ECDSA ASN.1 sig: %v", err)
		}
		byteLen := (key.Curve.Params().BitSize + 7) / 8
		rBytes := parsed.R.Bytes()
		sBytes := parsed.S.Bytes()
		for len(rBytes) < byteLen {
			rBytes = append([]byte{0}, rBytes...)
		}
		for len(sBytes) < byteLen {
			sBytes = append([]byte{0}, sBytes...)
		}
		sig = append(rBytes, sBytes...)
	default:
		t.Fatalf("liveSignJWS: unsupported algorithm %s", alg)
	}

	flat := struct {
		Protected string `json:"protected"`
		Payload   string `json:"payload"`
		Signature string `json:"signature"`
	}{
		Protected: protected,
		Payload:   encodedPayload,
		Signature: base64.RawURLEncoding.EncodeToString(sig),
	}
	out, err := json.Marshal(flat)
	if err != nil {
		t.Fatalf("marshal JWS flat: %v", err)
	}
	return out
}

// liveSignEAB creates a valid EAB inner JWS (RFC 8555 section 7.3.4).
// The EAB binds the account JWK to the EAB kid using HMAC-SHA256.
func liveSignEAB(t *testing.T, kid string, hmacKey []byte, accountJWK json.RawMessage, url string) []byte {
	t.Helper()

	header := map[string]string{
		"alg": "HS256",
		"kid": kid,
		"url": url,
	}
	headerJSON, err := json.Marshal(header)
	if err != nil {
		t.Fatalf("marshal EAB header: %v", err)
	}
	protected := base64.RawURLEncoding.EncodeToString(headerJSON)
	encodedPayload := base64.RawURLEncoding.EncodeToString(accountJWK)

	signingInput := []byte(protected + "." + encodedPayload)

	mac := hmac.New(sha256.New, hmacKey)
	mac.Write(signingInput)
	sig := mac.Sum(nil)

	flat := struct {
		Protected string `json:"protected"`
		Payload   string `json:"payload"`
		Signature string `json:"signature"`
	}{
		Protected: protected,
		Payload:   encodedPayload,
		Signature: base64.RawURLEncoding.EncodeToString(sig),
	}
	out, err := json.Marshal(flat)
	if err != nil {
		t.Fatalf("marshal EAB flat: %v", err)
	}
	return out
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

func TestLiveDirectory(t *testing.T) {
	skipIfNotLive(t)
	env := newLiveEnv(t)

	resp, err := env.client.Get(env.rawACMEURL("/directory"))
	if err != nil {
		t.Fatalf("GET directory: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		t.Fatalf("expected 200, got %d: %s", resp.StatusCode, body)
	}

	var dir map[string]interface{}
	if err := json.NewDecoder(resp.Body).Decode(&dir); err != nil {
		t.Fatalf("decode directory: %v", err)
	}

	for _, field := range []string{"newNonce", "newAccount", "newOrder"} {
		v, ok := dir[field]
		if !ok {
			t.Errorf("directory missing field %q", field)
			continue
		}
		s, ok := v.(string)
		if !ok || s == "" {
			t.Errorf("directory field %q is empty or not a string: %v", field, v)
		}
	}

	meta, ok := dir["meta"].(map[string]interface{})
	if !ok {
		t.Fatal("directory missing meta object")
	}
	ear, ok := meta["externalAccountRequired"].(bool)
	if !ok || !ear {
		t.Errorf("expected meta.externalAccountRequired=true, got %v", meta["externalAccountRequired"])
	}
}

func TestLiveNonce(t *testing.T) {
	skipIfNotLive(t)
	env := newLiveEnv(t)
	env.ensureDirectory()

	req, err := http.NewRequest(http.MethodHead, env.acmeURL("/new-nonce"), nil)
	if err != nil {
		t.Fatalf("build request: %v", err)
	}
	resp, err := env.client.Do(req)
	if err != nil {
		t.Fatalf("HEAD new-nonce: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	nonce := resp.Header.Get("Replay-Nonce")
	if nonce == "" {
		t.Fatal("Replay-Nonce header is empty")
	}

	// Verify a second request returns a different nonce.
	req2, _ := http.NewRequest(http.MethodHead, env.acmeURL("/new-nonce"), nil)
	resp2, err := env.client.Do(req2)
	if err != nil {
		t.Fatalf("second HEAD new-nonce: %v", err)
	}
	defer resp2.Body.Close()
	nonce2 := resp2.Header.Get("Replay-Nonce")
	if nonce2 == "" {
		t.Fatal("second Replay-Nonce header is empty")
	}
	if nonce == nonce2 {
		t.Error("two nonces should not be identical")
	}
}

func TestLiveCreateEABAndAccount(t *testing.T) {
	skipIfNotLive(t)
	env := newLiveEnv(t)

	// a) Create EAB via management API.
	kid, hmacKey := env.createEAB()
	if kid == "" {
		t.Fatal("EAB kid is empty")
	}
	if len(hmacKey) == 0 {
		t.Fatal("EAB hmac_key is empty")
	}

	// b) Get a nonce.
	nonce := env.getNonce()
	if nonce == "" {
		t.Fatal("nonce is empty")
	}

	// c) Generate an ES256 key pair.
	key, jwk := env.generateKey()

	// d) Construct EAB inner JWS.
	jwsAccountURL := env.jwsURL("/new-account")
	eabJWS := liveSignEAB(t, kid, hmacKey, jwk, jwsAccountURL)

	// e) Construct the new-account JWS with EAB.
	payload, err := json.Marshal(map[string]interface{}{
		"termsOfServiceAgreed":   true,
		"contact":               []string{"mailto:live-test@metacircular.net"},
		"externalAccountBinding": json.RawMessage(eabJWS),
	})
	if err != nil {
		t.Fatalf("marshal payload: %v", err)
	}

	header := map[string]interface{}{
		"alg":   "ES256",
		"nonce": nonce,
		"url":   jwsAccountURL,
		"jwk":   json.RawMessage(jwk),
	}
	body := liveSignJWS(t, key, "ES256", header, payload)

	// f) POST to new-account (use our reachable URL, not the JWS URL).
	resp, err := env.client.Post(env.acmeURL("/new-account"), "application/jose+json", strings.NewReader(string(body)))
	if err != nil {
		t.Fatalf("POST new-account: %v", err)
	}
	defer resp.Body.Close()
	respBody, _ := io.ReadAll(resp.Body)

	// g) Verify response.
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d: %s", resp.StatusCode, respBody)
	}

	location := resp.Header.Get("Location")
	if location == "" {
		t.Fatal("Location header missing from new-account response")
	}
	t.Logf("account created: %s", location)

	var acctResp map[string]interface{}
	if err := json.Unmarshal(respBody, &acctResp); err != nil {
		t.Fatalf("decode account response: %v", err)
	}
	status, _ := acctResp["status"].(string)
	if status != "valid" {
		t.Errorf("expected account status 'valid', got %q", status)
	}

	// Verify the EAB is now consumed — re-using it should fail.
	nonce2 := env.getNonce()
	eabJWS2 := liveSignEAB(t, kid, hmacKey, jwk, jwsAccountURL)
	key2, jwk2 := env.generateKey()
	payload2, _ := json.Marshal(map[string]interface{}{
		"termsOfServiceAgreed":   true,
		"contact":               []string{"mailto:live-test-2@metacircular.net"},
		"externalAccountBinding": json.RawMessage(eabJWS2),
	})
	header2 := map[string]interface{}{
		"alg":   "ES256",
		"nonce": nonce2,
		"url":   jwsAccountURL,
		"jwk":   json.RawMessage(jwk2),
	}
	body2 := liveSignJWS(t, key2, "ES256", header2, payload2)
	resp2, err := env.client.Post(env.acmeURL("/new-account"), "application/jose+json", strings.NewReader(string(body2)))
	if err != nil {
		t.Fatalf("POST new-account (reuse EAB): %v", err)
	}
	defer resp2.Body.Close()
	if resp2.StatusCode == http.StatusCreated {
		t.Error("expected EAB reuse to be rejected, but got 201")
	}
}

func TestLiveNewOrder(t *testing.T) {
	skipIfNotLive(t)
	env := newLiveEnv(t)
	env.ensureAccount()

	orderPayload, err := json.Marshal(map[string]interface{}{
		"identifiers": []map[string]string{
			{"type": "dns", "value": "live-test.svc.mcp.metacircular.net"},
		},
	})
	if err != nil {
		t.Fatalf("marshal order payload: %v", err)
	}

	resp, respBody := env.postSigned(env.acmeURL("/new-order"), env.jwsURL("/new-order"), orderPayload)

	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("expected 201, got %d: %s", resp.StatusCode, respBody)
	}

	var order map[string]interface{}
	if err := json.Unmarshal(respBody, &order); err != nil {
		t.Fatalf("decode order: %v", err)
	}

	status, _ := order["status"].(string)
	if status != "pending" {
		t.Errorf("expected order status 'pending', got %q", status)
	}

	identifiers, ok := order["identifiers"].([]interface{})
	if !ok || len(identifiers) == 0 {
		t.Fatal("order missing identifiers")
	}
	firstID, _ := identifiers[0].(map[string]interface{})
	if idType, _ := firstID["type"].(string); idType != "dns" {
		t.Errorf("expected identifier type 'dns', got %q", idType)
	}
	if idVal, _ := firstID["value"].(string); idVal != "live-test.svc.mcp.metacircular.net" {
		t.Errorf("expected identifier value 'live-test.svc.mcp.metacircular.net', got %q", idVal)
	}

	authzURLs, ok := order["authorizations"].([]interface{})
	if !ok || len(authzURLs) == 0 {
		t.Fatal("order missing authorizations")
	}
	t.Logf("order created with %d authorization(s)", len(authzURLs))

	location := resp.Header.Get("Location")
	if location == "" {
		t.Error("order response missing Location header")
	}
}

func TestLiveGetAuthz(t *testing.T) {
	skipIfNotLive(t)
	env := newLiveEnv(t)
	env.ensureAccount()

	// Create an order to get an authorization URL.
	orderPayload, _ := json.Marshal(map[string]interface{}{
		"identifiers": []map[string]string{
			{"type": "dns", "value": "live-authz-test.svc.mcp.metacircular.net"},
		},
	})

	resp, respBody := env.postSigned(env.acmeURL("/new-order"), env.jwsURL("/new-order"), orderPayload)
	if resp.StatusCode != http.StatusCreated {
		t.Fatalf("create order: status %d: %s", resp.StatusCode, respBody)
	}

	var order map[string]interface{}
	if err := json.Unmarshal(respBody, &order); err != nil {
		t.Fatalf("decode order: %v", err)
	}

	authzURLs, ok := order["authorizations"].([]interface{})
	if !ok || len(authzURLs) == 0 {
		t.Fatal("order has no authorizations")
	}
	authzURL, _ := authzURLs[0].(string)
	if authzURL == "" {
		t.Fatal("authorization URL is empty")
	}

	// POST-as-GET to the authorization URL.
	// authzURL uses the server's base URL; translate for HTTP request.
	reachableAuthzURL := strings.Replace(authzURL, env.serverBaseURL, env.acmeBaseURL, 1)
	authzResp, authzBody := env.postAsGet(reachableAuthzURL, authzURL)
	if authzResp.StatusCode != http.StatusOK {
		t.Fatalf("GET authz: status %d: %s", authzResp.StatusCode, authzBody)
	}

	var authz map[string]interface{}
	if err := json.Unmarshal(authzBody, &authz); err != nil {
		t.Fatalf("decode authz: %v", err)
	}

	authzStatus, _ := authz["status"].(string)
	if authzStatus != "pending" {
		t.Errorf("expected authz status 'pending', got %q", authzStatus)
	}

	identifier, _ := authz["identifier"].(map[string]interface{})
	if idType, _ := identifier["type"].(string); idType != "dns" {
		t.Errorf("expected identifier type 'dns', got %q", idType)
	}
	if idVal, _ := identifier["value"].(string); idVal != "live-authz-test.svc.mcp.metacircular.net" {
		t.Errorf("expected identifier value 'live-authz-test.svc.mcp.metacircular.net', got %q", idVal)
	}

	challenges, ok := authz["challenges"].([]interface{})
	if !ok || len(challenges) == 0 {
		t.Fatal("authz has no challenges")
	}

	// Verify we have http-01 and dns-01 challenge types.
	challengeTypes := make(map[string]bool)
	for _, c := range challenges {
		ch, _ := c.(map[string]interface{})
		cType, _ := ch["type"].(string)
		challengeTypes[cType] = true

		// Each challenge should have a token and status.
		token, _ := ch["token"].(string)
		if token == "" {
			t.Errorf("challenge %s missing token", cType)
		}
		cStatus, _ := ch["status"].(string)
		if cStatus == "" {
			t.Errorf("challenge %s missing status", cType)
		}
		cURL, _ := ch["url"].(string)
		if cURL == "" {
			t.Errorf("challenge %s missing url", cType)
		}
	}

	if !challengeTypes["http-01"] {
		t.Error("expected http-01 challenge type, not found")
	}
	if !challengeTypes["dns-01"] {
		t.Error("expected dns-01 challenge type, not found")
	}
	t.Logf("authorization has %d challenge(s): %v", len(challenges), challengeTypesList(challengeTypes))
}

func TestLiveFullFlow(t *testing.T) {
	skipIfNotLive(t)
	if os.Getenv("METACRYPT_LIVE_DNS_TEST") != "1" {
		t.Skip("set METACRYPT_LIVE_DNS_TEST=1 to run full ACME flow " +
			"(requires programmatic DNS updates to CoreDNS zones)")
	}

	// TODO: Implement full ACME flow once CoreDNS zone update automation
	// is in place. The flow would be:
	//   1. Create account
	//   2. Create order
	//   3. Get authorization + challenge
	//   4. Provision DNS-01 TXT record via CoreDNS API
	//   5. Respond to challenge
	//   6. Poll authorization until valid
	//   7. Finalize order with CSR
	//   8. Download certificate
	//   9. Verify certificate chain
	t.Fatal("not implemented")
}

// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------

// challengeTypesList extracts the keys from a map for logging.
func challengeTypesList(m map[string]bool) string {
	var types []string
	for k := range m {
		types = append(types, k)
	}
	return fmt.Sprintf("[%s]", strings.Join(types, ", "))
}
