package agentapi

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-controller/internal/util"
)

// Version-aware Supports (v0.115.0, pairs with agent v0.82.0's X-Felhom-Agent-Version): a KNOWN
// version decides by comparison with ZERO probe traffic; unknown/garbage stays on the v0.114.0
// probe path byte-identically.

// verProber implements SupportProber + AgentVersionReporter with a probe counter.
type verProber struct {
	ver    string
	probes int
}

func (p *verProber) NetVerifyStatus(context.Context) (NetVerifyStatus, error) {
	p.probes++
	return NetVerifyStatus{Phase: "none"}, nil // a live route (probe verdict would be Yes)
}
func (p *verProber) AgentVersion() string { return p.ver }

// --- B3.1: version known → compare path, probe count 0 -----------------------------------------
// Companion red-proof: drop the supportsByVersion short-circuit in Supports → probes becomes 1.
func TestSupports_VersionKnown_ComparesWithoutProbe(t *testing.T) {
	for _, tc := range []struct {
		ver  string
		want SupportState
	}{
		{"0.82.0", SupportYes},
		{"0.81.0", SupportYes},  // boundary: MinAgent itself qualifies
		{"1.0.0", SupportYes},   // numeric compare across majors
		{"0.100.0", SupportYes}, // numeric, NOT lexicographic (0.100 > 0.81)
		{"0.79.0", SupportNo},   // below MinAgent → No, still without probing
		{"0.80.9", SupportNo},
	} {
		t.Run(tc.ver, func(t *testing.T) {
			p := &verProber{ver: tc.ver}
			var sc SupportCache
			if got := sc.Supports(context.Background(), p, FeatureNetstorageVerify); got != tc.want {
				t.Errorf("Supports(ver=%s) = %v, want %v", tc.ver, got, tc.want)
			}
			if p.probes != 0 {
				t.Errorf("version-known path must NOT probe (ver=%s, probes=%d)", tc.ver, p.probes)
			}
		})
	}
}

// --- B3.2: garbage/absent version → the probe fallback, byte-identical --------------------------
// Companion red-proof: trust the unvalidated header text (compare without ParseVersion error
// handling) → the garbage rows would refuse or panic instead of probing.
func TestSupports_GarbageOrNoVersion_ProbeFallback(t *testing.T) {
	for _, ver := range []string{"", "dev", "0.82", "0.82.0-rc1", "v0.82.0-beta", "evil;rm -rf", "9999999999999999999999.0.0"} {
		t.Run("ver="+ver, func(t *testing.T) {
			p := &verProber{ver: ver}
			var sc SupportCache
			got := sc.Supports(context.Background(), p, FeatureNetstorageVerify)
			if p.probes != 1 {
				t.Fatalf("unusable version %q must fall back to EXACTLY one probe, got %d", ver, p.probes)
			}
			if got != SupportYes { // the fake's route answers → the probe decides Yes
				t.Errorf("probe fallback verdict = %v, want SupportYes", got)
			}
			// Second call: the probe verdict is cached (the v0.114.0 behavior, unchanged).
			_ = sc.Supports(context.Background(), p, FeatureNetstorageVerify)
			if p.probes != 1 {
				t.Errorf("cached probe verdict must not re-probe (probes=%d)", p.probes)
			}
		})
	}
}

// A prober that does NOT implement AgentVersionReporter (the web fakes' shape) keeps the pure
// v0.114.0 behavior — the interface assertion must not change anything for it.
func TestSupports_NonReporterProber_Unchanged(t *testing.T) {
	p := &fakeProber{} // the existing v0.114.0 test fake (features_test.go)
	var sc SupportCache
	if got := sc.Supports(context.Background(), p, FeatureNetstorageVerify); got != SupportYes {
		t.Fatalf("non-reporter prober = %v, want SupportYes via probe", got)
	}
	if p.calls != 1 {
		t.Errorf("non-reporter prober must probe exactly once, got %d", p.calls)
	}
}

// --- B3.4: the ONE comparator, table-driven (incl. pre-release suffixes) ------------------------
func TestVersionComparator_Table(t *testing.T) {
	lt := func(a, b string) {
		t.Helper()
		av, err1 := util.ParseVersion(a)
		bv, err2 := util.ParseVersion(b)
		if err1 != nil || err2 != nil {
			t.Fatalf("parse %q/%q: %v %v", a, b, err1, err2)
		}
		if av.Compare(bv) != -1 || bv.Compare(av) != 1 {
			t.Errorf("want %s < %s", a, b)
		}
	}
	lt("0.81.0", "0.82.0")
	lt("0.81.0", "0.100.0") // numeric minor, not lexicographic
	lt("0.99.9", "1.0.0")
	lt("1.2.3", "1.2.10")
	if v, err := util.ParseVersion("v0.82.0"); err != nil || v.Raw != "0.82.0" {
		t.Errorf("v-prefix must parse: %v %v", v, err)
	}
	if eq, _ := util.ParseVersion("0.81.0"); eq.Compare(eq) != 0 {
		t.Error("equal versions must compare 0")
	}
	// Pre-release suffixes are REJECTED by the house comparator — in Supports they mean "fall back
	// to the probe", never a trusted comparison.
	for _, bad := range []string{"1.2.3-rc1", "dev", "latest", "", "1.2", "a.b.c"} {
		if _, err := util.ParseVersion(bad); err == nil {
			t.Errorf("ParseVersion(%q) must error", bad)
		}
	}
}

// --- B3.5: wire-level — the header decides over the probe through the REAL pinned client --------
// A ROUTELESS agent (probe would say No) that sends X-Felhom-Agent-Version 9.9.9 must be judged by
// the VERSION (Yes): proof the channel takes precedence end-to-end, and that capture happens on an
// ordinary (even failing) call.
func TestClient_VersionHeaderWins_WireLevel(t *testing.T) {
	mux := http.NewServeMux() // NO /netstorage/verify-status (the pre-0.81 route shape)
	mux.HandleFunc("/storage", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Felhom-Agent-Version", "9.9.9")
		_, _ = w.Write([]byte(`{"ok":true,"data":{"vmid":1,"mounts":[]}}`))
	})
	wrapped := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Felhom-Agent-Version", "9.9.9") // every response, like the agent's wrap
		mux.ServeHTTP(w, r)
	})
	srv := httptest.NewTLSServer(wrapped)
	defer srv.Close()
	fp := sha256.Sum256(srv.Certificate().Raw)
	c, err := New(strings.TrimPrefix(srv.URL, "https://"), "test-token", hex.EncodeToString(fp[:]))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	if _, err := c.Storage(context.Background()); err != nil {
		t.Fatalf("storage: %v", err)
	}
	if got := c.AgentVersion(); got != "9.9.9" {
		t.Fatalf("AgentVersion = %q, want 9.9.9 (passive capture)", got)
	}
	if got := c.Supports(context.Background(), FeatureNetstorageVerify); got != SupportYes {
		t.Errorf("Supports = %v, want SupportYes via version despite the missing probe route", got)
	}
}

// A GARBAGE header must never be captured: the routeless agent stays on the probe → SupportNo.
func TestClient_GarbageHeaderIgnored_WireLevel(t *testing.T) {
	wrapped := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Felhom-Agent-Version", "not-a-version;x")
		http.NotFound(w, r)
	})
	srv := httptest.NewTLSServer(wrapped)
	defer srv.Close()
	fp := sha256.Sum256(srv.Certificate().Raw)
	c, err := New(strings.TrimPrefix(srv.URL, "https://"), "test-token", hex.EncodeToString(fp[:]))
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	_, _ = c.Storage(context.Background()) // 404s; the garbage header must be dropped at capture
	if got := c.AgentVersion(); got != "" {
		t.Fatalf("AgentVersion = %q, want empty (strict validation at capture)", got)
	}
	if got := c.Supports(context.Background(), FeatureNetstorageVerify); got != SupportNo {
		t.Errorf("Supports = %v, want SupportNo via the probe fallback", got)
	}
}
