package acmecert

import (
	"context"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"math/big"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"testing"
	"time"

	"github.com/go-acme/lego/v4/registration"
	"gopkg.in/ini.v1"

	"mailgoserver/internal/tlsutil"
)

func TestLoadOrCreateAccountGeneratesAndPersistsKey(t *testing.T) {
	dir := t.TempDir()

	user1, err := loadOrCreateAccount(dir, "admin@example.com")
	if err != nil {
		t.Fatal(err)
	}
	if user1.Registration != nil {
		t.Fatal("expected no registration on a brand-new account")
	}
	if user1.GetPrivateKey() == nil {
		t.Fatal("expected a generated private key")
	}

	// Reload: must reuse the same key, not generate a new one.
	user2, err := loadOrCreateAccount(dir, "admin@example.com")
	if err != nil {
		t.Fatal(err)
	}
	keyBytes1, _ := os.ReadFile(accountKeyPath(dir))
	if len(keyBytes1) == 0 {
		t.Fatal("expected a persisted key file")
	}
	// Re-reading shouldn't rewrite the file with different bytes.
	keyBytes2, _ := os.ReadFile(accountKeyPath(dir))
	if string(keyBytes1) != string(keyBytes2) {
		t.Fatal("expected the same key to be reused across loads")
	}
	_ = user2
}

func TestSaveRegistrationRoundTrip(t *testing.T) {
	dir := t.TempDir()
	reg := &registration.Resource{URI: "https://example.com/acme/acct/123"}
	if err := saveRegistration(dir, reg); err != nil {
		t.Fatal(err)
	}

	user, err := loadOrCreateAccount(dir, "admin@example.com")
	if err != nil {
		t.Fatal(err)
	}
	if user.Registration == nil || user.Registration.URI != reg.URI {
		t.Fatalf("expected registration to round-trip, got %+v", user.Registration)
	}
}

// writeFixtureCert writes a minimal self-signed cert with the given expiry to certFile
// (no matching key needed — NeedsRenewal only reads the cert).
func writeFixtureCert(t *testing.T, certFile string, notAfter time.Time) {
	t.Helper()
	priv, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	tmpl := x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject:      pkix.Name{CommonName: "test"},
		NotBefore:    time.Now().Add(-time.Hour),
		NotAfter:     notAfter,
	}
	der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
	if err != nil {
		t.Fatal(err)
	}
	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
	if err := os.WriteFile(certFile, pemBytes, 0o644); err != nil {
		t.Fatal(err)
	}
}

func TestNeedsRenewal(t *testing.T) {
	dir := t.TempDir()
	mgr := &Manager{Cfg: ini.Empty(), CertFile: filepath.Join(dir, "server.crt")}

	writeFixtureCert(t, mgr.CertFile, time.Now().Add(200*24*time.Hour))
	if needs, err := mgr.NeedsRenewal(); err != nil || needs {
		t.Fatalf("expected NeedsRenewal=false for a cert expiring in 200 days, got %v (err=%v)", needs, err)
	}

	writeFixtureCert(t, mgr.CertFile, time.Now().Add(5*24*time.Hour))
	if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
		t.Fatalf("expected NeedsRenewal=true for a cert expiring in 5 days, got %v (err=%v)", needs, err)
	}
}

func TestNeedsRenewalTrueForSelfSignedPlaceholderEvenWithLongExpiry(t *testing.T) {
	dir := t.TempDir()
	certFile := filepath.Join(dir, "server.crt")
	keyFile := filepath.Join(dir, "server.key")
	if err := tlsutil.GenerateSelfSignedCert(certFile, keyFile); err != nil {
		t.Fatal(err)
	}
	mgr := &Manager{Cfg: ini.Empty(), CertFile: certFile}

	// tlsutil's self-signed cert is valid for a year — a pure expiry check would say
	// "no renewal needed," which is exactly the bug: a restart must still recognize
	// this as "no real certificate obtained yet" and trigger the first real obtain.
	needs, err := mgr.NeedsRenewal()
	if err != nil {
		t.Fatal(err)
	}
	if !needs {
		t.Fatal("expected NeedsRenewal=true for the self-signed placeholder despite its long expiry")
	}
}

func TestNeedsRenewalUsesShortThresholdForHTTP01IncludeIP(t *testing.T) {
	dir := t.TempDir()
	certFile := filepath.Join(dir, "server.crt")
	cfg := ini.Empty()
	cfg.Section("LetsEncryptHTTP").Key("include_ip").SetValue("true")
	mgr := &Manager{Cfg: cfg, Section: "LetsEncryptHTTP", ChallengeType: "http-01", CertFile: certFile}

	// 4 days left: not within the 1-day short threshold, even though it WOULD be
	// within the normal 30-day one — proves the short threshold is actually in effect.
	writeFixtureCert(t, certFile, time.Now().Add(4*24*time.Hour))
	if needs, err := mgr.NeedsRenewal(); err != nil || needs {
		t.Fatalf("expected NeedsRenewal=false with 4 days left under the short threshold, got %v (err=%v)", needs, err)
	}

	// 12 hours left: within the 1-day short threshold.
	writeFixtureCert(t, certFile, time.Now().Add(12*time.Hour))
	if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
		t.Fatalf("expected NeedsRenewal=true with 12 hours left, got %v (err=%v)", needs, err)
	}
}

func TestNeedsRenewalUsesNormalThresholdForHTTP01WithoutIncludeIP(t *testing.T) {
	dir := t.TempDir()
	certFile := filepath.Join(dir, "server.crt")
	cfg := ini.Empty()
	cfg.Section("LetsEncryptHTTP").Key("include_ip").SetValue("false")
	mgr := &Manager{Cfg: cfg, Section: "LetsEncryptHTTP", ChallengeType: "http-01", CertFile: certFile}

	// 4 days left: within the normal 30-day threshold — confirms include_ip=false
	// gets the normal threshold, not the short one.
	writeFixtureCert(t, certFile, time.Now().Add(4*24*time.Hour))
	if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
		t.Fatalf("expected NeedsRenewal=true with 4 days left under the normal threshold, got %v (err=%v)", needs, err)
	}
}

func TestNeedsRenewalMissingCertIsTrue(t *testing.T) {
	mgr := &Manager{Cfg: ini.Empty(), CertFile: filepath.Join(t.TempDir(), "does-not-exist.crt")}
	needs, err := mgr.NeedsRenewal()
	if err != nil {
		t.Fatal(err)
	}
	if !needs {
		t.Fatal("expected NeedsRenewal=true when no certificate exists yet")
	}
}

func TestBuildDNSProviderUnknownName(t *testing.T) {
	cfg := ini.Empty()
	cfg.Section("LetsEncrypt").Key("dns_provider").SetValue("not-a-real-provider")
	if _, err := buildDNSProvider(cfg); err == nil {
		t.Fatal("expected an error for an unknown DNS provider name")
	}
}

func TestBuildDNSProviderDigitalOceanRequiresToken(t *testing.T) {
	cfg := ini.Empty()
	cfg.Section("LetsEncrypt").Key("dns_provider").SetValue("digitalocean")
	// AuthToken deliberately left blank — DigitalOcean's constructor validates this
	// locally (no network call) and errors immediately.
	if _, err := buildDNSProvider(cfg); err == nil {
		t.Fatal("expected an error when digitalocean_api_token is blank")
	}
}

func TestDetectWANIP(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("203.0.113.42\n"))
	}))
	defer srv.Close()
	old := wanIPServiceURL
	wanIPServiceURL = srv.URL
	defer func() { wanIPServiceURL = old }()

	ip, err := DetectWANIP(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if ip != "203.0.113.42" {
		t.Fatalf("expected trimmed IP %q, got %q", "203.0.113.42", ip)
	}
}

func TestResolveIdentifiersDNS01IgnoresIPSettings(t *testing.T) {
	cfg := ini.Empty()
	sec := cfg.Section("LetsEncrypt")
	sec.Key("domains").SetValue("mail.example.com")
	sec.Key("include_ip").SetValue("true") // should be ignored outside http-01
	mgr := &Manager{Cfg: cfg, Section: "LetsEncrypt", ChallengeType: "dns-01"}

	got, err := mgr.resolveIdentifiers(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 || got[0] != "mail.example.com" {
		t.Fatalf("expected just the domain, got %v", got)
	}
}

func TestDomainsAreLowercased(t *testing.T) {
	cfg := ini.Empty()
	sec := cfg.Section("LetsEncrypt")
	sec.Key("domains").SetValue("adsl-1-2-3-4.example.ISP.COM, Mail.Example.com")
	mgr := &Manager{Cfg: cfg, Section: "LetsEncrypt", ChallengeType: "dns-01"}

	got := mgr.domains()
	want := []string{"adsl-1-2-3-4.example.isp.com", "mail.example.com"}
	if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("expected lowercased %v, got %v", want, got)
	}
}

func TestResolveIdentifiersHTTP01WithManualIPOverride(t *testing.T) {
	cfg := ini.Empty()
	sec := cfg.Section("LetsEncryptHTTP")
	sec.Key("domains").SetValue("mail.example.com")
	sec.Key("include_ip").SetValue("true")
	sec.Key("ip_override").SetValue("198.51.100.7")
	mgr := &Manager{Cfg: cfg, Section: "LetsEncryptHTTP", ChallengeType: "http-01"}

	got, err := mgr.resolveIdentifiers(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	want := []string{"mail.example.com", "198.51.100.7"}
	if len(got) != 2 || got[0] != want[0] || got[1] != want[1] {
		t.Fatalf("expected %v, got %v", want, got)
	}
}

func TestResolveIdentifiersHTTP01AutodetectsIPWhenOverrideBlank(t *testing.T) {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("203.0.113.99"))
	}))
	defer srv.Close()
	old := wanIPServiceURL
	wanIPServiceURL = srv.URL
	defer func() { wanIPServiceURL = old }()

	cfg := ini.Empty()
	sec := cfg.Section("LetsEncryptHTTP")
	sec.Key("include_ip").SetValue("true")
	mgr := &Manager{Cfg: cfg, Section: "LetsEncryptHTTP", ChallengeType: "http-01"}

	got, err := mgr.resolveIdentifiers(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 1 || got[0] != "203.0.113.99" {
		t.Fatalf("expected autodetected IP as the sole identifier, got %v", got)
	}
}

func TestResolveIdentifiersEmptyIsError(t *testing.T) {
	mgr := &Manager{Cfg: ini.Empty(), Section: "LetsEncrypt", ChallengeType: "dns-01"}
	if _, err := mgr.resolveIdentifiers(context.Background()); err == nil {
		t.Fatal("expected an error when no domains and no IP are configured")
	}
}

func TestBuildDNSProviderCloudflare(t *testing.T) {
	cfg := ini.Empty()
	sec := cfg.Section("LetsEncrypt")
	sec.Key("dns_provider").SetValue("cloudflare")
	sec.Key("cloudflare_api_token").SetValue("fake-token-for-local-construction-only")
	provider, err := buildDNSProvider(cfg)
	if err != nil {
		t.Fatalf("expected local provider construction to succeed without a network call, got: %v", err)
	}
	if provider == nil {
		t.Fatal("expected a non-nil provider")
	}
}
