package endpoint

import (
	"errors"
	"testing"

	"github.com/wangjia/pangolin/infra/domains/tools/internal/notice"
	"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
)

func buildSigned(t *testing.T, p Payload, keyID string, version uint64) ([]byte, sign.KeyRing) {
	t.Helper()
	pub, priv, err := sign.GenerateKey()
	if err != nil {
		t.Fatal(err)
	}
	env, err := Build(p, keyID, version, "2026-06-13T00:00:00Z", priv)
	if err != nil {
		t.Fatalf("Build: %v", err)
	}
	raw, err := sign.Marshal(env)
	if err != nil {
		t.Fatal(err)
	}
	return raw, sign.KeyRing{keyID: pub}
}

func validPayload() Payload {
	return Payload{
		APIDomains: []string{"api-b.example.net", "api-a.example.com"},
		MirrorURLs: []string{"https://m1.example.com/endpoints.v1.json"},
	}
}

func TestBuildVerifyRoundTrip(t *testing.T) {
	raw, ring := buildSigned(t, validPayload(), "k1", 3)
	env, p, err := VerifyDocument(raw, ring, 0)
	if err != nil {
		t.Fatalf("VerifyDocument: %v", err)
	}
	if env.Version != 3 {
		t.Fatalf("version = %d", env.Version)
	}
	// Normalization should have sorted the domains.
	if p.APIDomains[0] != "api-a.example.com" {
		t.Fatalf("domains not normalized/sorted: %v", p.APIDomains)
	}
}

func TestRollbackRejected(t *testing.T) {
	raw, ring := buildSigned(t, validPayload(), "k1", 5)
	// Client already trusts version 5; a v5 (replay) or lower must be rejected.
	if _, _, err := VerifyDocument(raw, ring, 5); !errors.Is(err, ErrRollback) {
		t.Fatalf("want ErrRollback for equal version, got %v", err)
	}
	if _, _, err := VerifyDocument(raw, ring, 9); !errors.Is(err, ErrRollback) {
		t.Fatalf("want ErrRollback for lower version, got %v", err)
	}
	// A newer current baseline that is actually older than doc is accepted.
	if _, _, err := VerifyDocument(raw, ring, 4); err != nil {
		t.Fatalf("v5 doc over current=4 should pass, got %v", err)
	}
}

func TestTamperRejected(t *testing.T) {
	raw, ring := buildSigned(t, validPayload(), "k1", 1)
	// Flip a byte inside the JSON.
	tampered := make([]byte, len(raw))
	copy(tampered, raw)
	for i := range tampered {
		if tampered[i] == 'a' {
			tampered[i] = 'b'
			break
		}
	}
	if _, _, err := VerifyDocument(tampered, ring, 0); err == nil {
		t.Fatal("tampered document accepted")
	}
}

func TestKeyRotationTransition(t *testing.T) {
	oldPub, _, _ := sign.GenerateKey()
	newPub, newPriv, _ := sign.GenerateKey()
	env, err := Build(validPayload(), "v2", 2, "2026-06-13T00:00:00Z", newPriv)
	if err != nil {
		t.Fatal(err)
	}
	raw, _ := sign.Marshal(env)
	// Rotation window: client carries both old and new public keys.
	ring := sign.KeyRing{"v1": oldPub, "v2": newPub}
	if _, _, err := VerifyDocument(raw, ring, 0); err != nil {
		t.Fatalf("rotation window verify failed: %v", err)
	}
}

func TestValidationRejectsEmptyDomains(t *testing.T) {
	pub, priv, _ := sign.GenerateKey()
	_ = pub
	if _, err := Build(Payload{MirrorURLs: []string{"https://m/x"}}, "k1", 1, "t", priv); err == nil {
		t.Fatal("want error for empty api_domains")
	}
}

func TestValidationRejectsBadMirrorURL(t *testing.T) {
	_, priv, _ := sign.GenerateKey()
	p := Payload{APIDomains: []string{"a.example.com"}, MirrorURLs: []string{"not-a-url"}}
	if _, err := Build(p, "k1", 1, "t", priv); err == nil {
		t.Fatal("want error for bad mirror url")
	}
}

func TestNoticeInPayloadValidated(t *testing.T) {
	_, priv, _ := sign.GenerateKey()
	p := validPayload()
	p.Notice = &notice.Notice{ID: "n1", Level: "bogus", TitleZH: "x", TitleEn: "x", PublishedAt: "t"}
	if _, err := Build(p, "k1", 1, "t", priv); err == nil {
		t.Fatal("want error for invalid notice level")
	}
}

func TestChannelPreserved(t *testing.T) {
	p := validPayload()
	p.Channel = "play-store"
	raw, ring := buildSigned(t, p, "k1", 1)
	_, got, err := VerifyDocument(raw, ring, 0)
	if err != nil {
		t.Fatal(err)
	}
	if got.Channel != "play-store" {
		t.Fatalf("channel lost: %q", got.Channel)
	}
}
