package apps

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"
)

// genTestKey returns a fresh Ed25519 keypair for tests so the test
// suite never depends on the embedded production key.
func genTestKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
	t.Helper()
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("GenerateKey: %v", err)
	}
	return pub, priv
}

func TestParseEd25519PublicKeyPEM_RoundTrip(t *testing.T) {
	pub, _ := genTestKey(t)
	derBytes, err := x509.MarshalPKIXPublicKey(pub)
	if err != nil {
		t.Fatalf("MarshalPKIXPublicKey: %v", err)
	}
	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: derBytes})

	parsed, err := ParsePubKeyPEM(pemBytes)
	if err != nil {
		t.Fatalf("parse: %v", err)
	}
	if !pub.Equal(parsed) {
		t.Errorf("round-trip pubkey mismatch")
	}
}

func TestParseEd25519PublicKeyPEM_RejectsRSA(t *testing.T) {
	// PEM containing a non-Ed25519 key should error rather than
	// silently coerce. Use a hand-crafted bad PEM block.
	bad := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: []byte("not a valid SubjectPublicKeyInfo")})
	if _, err := ParsePubKeyPEM(bad); err == nil {
		t.Error("ParsePubKeyPEM accepted malformed PEM, want error")
	}
}

func TestParseEd25519PublicKeyPEM_RejectsWrongType(t *testing.T) {
	pub, _ := genTestKey(t)
	derBytes, _ := x509.MarshalPKIXPublicKey(pub)
	wrongType := pem.EncodeToMemory(&pem.Block{Type: "RSA PUBLIC KEY", Bytes: derBytes})
	if _, err := ParsePubKeyPEM(wrongType); err == nil {
		t.Error("ParsePubKeyPEM accepted wrong PEM Type, want error")
	}
}

func TestVerifyEd25519_ValidSignature(t *testing.T) {
	pub, priv := genTestKey(t)
	msg := []byte("the artifact bytes")
	sig := ed25519.Sign(priv, msg)
	if err := VerifyEd25519(pub, msg, sig); err != nil {
		t.Errorf("VerifyEd25519 rejected a valid signature: %v", err)
	}
}

func TestVerifyEd25519_TamperedMessage(t *testing.T) {
	pub, priv := genTestKey(t)
	original := []byte("the artifact bytes")
	tampered := []byte("the artifact byteX")
	sig := ed25519.Sign(priv, original)
	if err := VerifyEd25519(pub, tampered, sig); err == nil {
		t.Error("VerifyEd25519 accepted a tampered message, want error")
	}
}

func TestVerifyEd25519_WrongKey(t *testing.T) {
	_, priv := genTestKey(t)
	otherPub, _ := genTestKey(t)
	msg := []byte("the artifact bytes")
	sig := ed25519.Sign(priv, msg)
	if err := VerifyEd25519(otherPub, msg, sig); err == nil {
		t.Error("VerifyEd25519 accepted a signature from the wrong key, want error")
	}
}

func TestVerifyEd25519_MalformedSignature(t *testing.T) {
	pub, _ := genTestKey(t)
	msg := []byte("hello")
	cases := [][]byte{
		nil,                      // empty
		make([]byte, 32),         // too short
		make([]byte, 100),        // too long
		make([]byte, 64),         // right length, wrong contents
	}
	for i, sig := range cases {
		if err := VerifyEd25519(pub, msg, sig); err == nil {
			t.Errorf("case %d: VerifyEd25519 accepted malformed signature of length %d, want error", i, len(sig))
		}
	}
}

func TestVerifyEd25519_NilKey(t *testing.T) {
	if err := VerifyEd25519(nil, []byte("x"), make([]byte, 64)); err == nil {
		t.Error("VerifyEd25519(nil, ...) accepted, want error")
	}
}

// TestFetcher_AcceptsValidSignature: end-to-end. Server publishes
// an artifact and a valid .sig; fetcher accepts and caches.
func TestFetcher_AcceptsValidSignature(t *testing.T) {
	pub, priv := genTestKey(t)
	body := []byte("<!doctype html><html><body>signed artifact</body></html>")
	sig := ed25519.Sign(priv, body)

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/archive.html":
			w.Header().Set("Content-Type", "text/html")
			_, _ = w.Write(body)
		case "/archive.html.sig":
			w.Header().Set("Content-Type", "application/octet-stream")
			_, _ = w.Write(sig)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	cache, err := NewCache(filepath.Join(t.TempDir(), "_app"))
	if err != nil {
		t.Fatalf("NewCache: %v", err)
	}
	f := NewFetcher(cache, nil)
	f.VerifyKey = pub // override the embedded production key
	got, err := f.Fetch(context.Background(), srv.URL+"/archive.html")
	if err != nil {
		t.Fatalf("Fetch failed: %v", err)
	}
	if string(got) != string(body) {
		t.Errorf("body mismatch")
	}
	// Cache hit on second call.
	if !cache.Has(srv.URL + "/archive.html") {
		t.Error("expected cache to contain artifact after successful verification")
	}
}

// TestFetcher_RejectsTamperedBody: the published .sig is valid but
// the body has been changed by a hypothetical mitm. Fetcher must
// reject and NOT cache the tampered bytes.
func TestFetcher_RejectsTamperedBody(t *testing.T) {
	pub, priv := genTestKey(t)
	original := []byte("<!doctype html>genuine")
	sig := ed25519.Sign(priv, original)
	tampered := []byte("<!doctype html>injected")

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/archive.html":
			_, _ = w.Write(tampered)
		case "/archive.html.sig":
			_, _ = w.Write(sig)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	cache, err := NewCache(filepath.Join(t.TempDir(), "_app"))
	if err != nil {
		t.Fatalf("NewCache: %v", err)
	}
	f := NewFetcher(cache, nil)
	f.VerifyKey = pub
	_, err = f.Fetch(context.Background(), srv.URL+"/archive.html")
	if err == nil {
		t.Fatal("Fetch accepted tampered body, want error")
	}
	if !strings.Contains(err.Error(), "signature") {
		t.Errorf("error %q does not mention signature", err)
	}
	if cache.Has(srv.URL + "/archive.html") {
		t.Error("tampered bytes were cached; verifier must not write to cache on rejection")
	}
}

// TestFetcher_RejectsMissingSignature: artifact published but no .sig
// alongside (HTTP 404). Strict mode → reject.
func TestFetcher_RejectsMissingSignature(t *testing.T) {
	pub, _ := genTestKey(t)
	body := []byte("body without sig")

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/archive.html":
			_, _ = w.Write(body)
		case "/archive.html.sig":
			http.NotFound(w, r)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	cache, _ := NewCache(filepath.Join(t.TempDir(), "_app"))
	f := NewFetcher(cache, nil)
	f.VerifyKey = pub
	_, err := f.Fetch(context.Background(), srv.URL+"/archive.html")
	if err == nil {
		t.Fatal("Fetch accepted unsigned artifact, want error")
	}
	if !strings.Contains(err.Error(), "404") && !strings.Contains(err.Error(), "signature") {
		t.Errorf("error %q does not mention 404 or signature", err)
	}
	if cache.Has(srv.URL + "/archive.html") {
		t.Error("unsigned bytes were cached; verifier must reject before caching")
	}
}

// TestFetcher_RejectsWrongKeySignature: .sig present, well-formed,
// but signed by a different key than f.VerifyKey trusts.
func TestFetcher_RejectsWrongKeySignature(t *testing.T) {
	trustedPub, _ := genTestKey(t)
	_, attackerPriv := genTestKey(t)
	body := []byte("body signed by an untrusted key")
	sig := ed25519.Sign(attackerPriv, body)

	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/archive.html":
			_, _ = w.Write(body)
		case "/archive.html.sig":
			_, _ = w.Write(sig)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	cache, _ := NewCache(filepath.Join(t.TempDir(), "_app"))
	f := NewFetcher(cache, nil)
	f.VerifyKey = trustedPub
	_, err := f.Fetch(context.Background(), srv.URL+"/archive.html")
	if err == nil {
		t.Fatal("Fetch accepted wrong-key-signed artifact, want error")
	}
	if cache.Has(srv.URL + "/archive.html") {
		t.Error("wrong-key-signed bytes were cached")
	}
}
