package certreload

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// TestServerGetCertificatePicksUpNewFile writes an initial cert/key
// pair, starts the reloader, then overwrites the files with a new pair
// and asserts that the callback eventually returns the new certificate.
func TestServerGetCertificatePicksUpNewFile(t *testing.T) {
	dir := t.TempDir()
	certPath := filepath.Join(dir, "tls.crt")
	keyPath := filepath.Join(dir, "tls.key")

	if err := writeSelfSigned(certPath, keyPath, "first"); err != nil {
		t.Fatalf("write first cert: %v", err)
	}

	// Use a short refresh window so the test doesn't wait 5h.
	getCert, provider, err := newServerGetCertificate(certPath, keyPath, 100*time.Millisecond)
	if err != nil {
		t.Fatalf("newServerGetCertificate: %v", err)
	}
	defer provider.Close()

	c1, err := getCert(nil)
	if err != nil {
		t.Fatalf("initial getCert: %v", err)
	}
	first, err := x509.ParseCertificate(c1.Certificate[0])
	if err != nil {
		t.Fatalf("parse first cert: %v", err)
	}
	if first.Subject.CommonName != "first" {
		t.Fatalf("want CN=first, got %q", first.Subject.CommonName)
	}

	// Rotate the files. pemfile watches mtime; sleep so the new files have
	// a later mtime even on filesystems with 1s granularity.
	time.Sleep(1100 * time.Millisecond)
	if err := writeSelfSigned(certPath, keyPath, "second"); err != nil {
		t.Fatalf("write second cert: %v", err)
	}

	// The pemfile poller can briefly observe a mismatched cert/key pair
	// during rotation (cert written before key). Tolerate transient errors
	// and only fail at the deadline; keep the last error for diagnostics.
	var lastErr error
	deadline := time.Now().Add(5 * time.Second)
	for time.Now().Before(deadline) {
		c2, err := getCert(nil)
		if err != nil {
			lastErr = err
			time.Sleep(100 * time.Millisecond)
			continue
		}
		parsed, err := x509.ParseCertificate(c2.Certificate[0])
		if err != nil {
			lastErr = err
			time.Sleep(100 * time.Millisecond)
			continue
		}
		if parsed.Subject.CommonName == "second" {
			return
		}
		time.Sleep(100 * time.Millisecond)
	}
	t.Fatalf("reloader did not pick up rotated cert within timeout; last error: %v", lastErr)
}

func writeSelfSigned(certPath, keyPath, commonName string) error {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return err
	}
	tmpl := &x509.Certificate{
		SerialNumber: big.NewInt(time.Now().UnixNano()),
		Subject:      pkix.Name{CommonName: commonName},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     time.Now().Add(time.Hour),
		KeyUsage:     x509.KeyUsageDigitalSignature,
		ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		return err
	}
	certOut, err := os.Create(certPath)
	if err != nil {
		return err
	}
	if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
		certOut.Close()
		return err
	}
	if err := certOut.Close(); err != nil {
		return err
	}
	keyDER, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		return err
	}
	keyOut, err := os.Create(keyPath)
	if err != nil {
		return err
	}
	if err := pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil {
		keyOut.Close()
		return err
	}
	return keyOut.Close()
}
