package web

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"

	"gitea.dooplex.hu/admin/felhom-hub/internal/gitea"
	"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)

// The installability gate (R-273). On 2026-08-08 agent v0.128.0 was published as a package and
// never git-tagged; the hub vouched it, and every fresh install and reinstall died at step 5 of 8,
// as root, on a virgin machine, for most of a day. These tests pin the check at the moment of risk.
//
// Each scenario names the WRONG outcome it exists to prevent, because a test whose failure mode is
// unstated is a test nobody can weigh.

// fakeRegistry serves the two URLs the gate probes. Anything not explicitly present 404s, which is
// the shape a pruned package or an unpushed tag really has.
type fakeRegistry struct {
	tagFiles map[string]bool // "<repo>/<tag>/<path>"
	packages map[string]bool // "<pkg>/<version>"
	status   int             // when non-zero, every request answers with this instead (E)
	// downloadGone models R-287: the version's METADATA still answers while the file itself is
	// gone. Keyed "<pkg>/<version>"; consulted only on the download route.
	downloadGone map[string]bool
	// shaless models a version whose metadata answers but carries no sha256.
	shaless map[string]bool
	hits     int
}

func (f *fakeRegistry) start(t *testing.T) *gitea.Client {
	t.Helper()
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		f.hits++
		if f.status != 0 {
			w.WriteHeader(f.status)
			return
		}
		p := strings.TrimPrefix(r.URL.Path, "/")
		// Package METADATA (api/v1/packages/<owner>/generic/<pkg>/<ver>/files) — what
		// resolveArtifactSHA calls before the gate ever runs. Serving it keeps these tests about
		// the installability gate rather than about sha resolution.
		if strings.HasPrefix(p, "api/v1/packages/") && strings.HasSuffix(p, "/files") {
			seg := strings.Split(p, "/")
			if len(seg) >= 8 && f.packages[seg[5]+"/"+seg[6]] {
				sha := strings.Repeat("c", 64)
				if f.shaless[seg[5]+"/"+seg[6]] {
					sha = ""
				}
				w.Header().Set("Content-Type", "application/json")
				_, _ = w.Write([]byte(`[{"name":"` + seg[5] + `","sha256":"` + sha + `"}]`))
				return
			}
			w.WriteHeader(http.StatusNotFound)
			return
		}
		// raw/tag:  <owner>/<repo>/raw/tag/<tag>/<path...>
		if i := strings.Index(p, "/raw/tag/"); i >= 0 {
			repo := p[:i]
			repo = repo[strings.Index(repo, "/")+1:]
			rest := p[i+len("/raw/tag/"):]
			slash := strings.Index(rest, "/")
			if slash > 0 && f.tagFiles[repo+"/"+rest[:slash]+"/"+rest[slash+1:]] {
				w.WriteHeader(http.StatusOK)
				return
			}
			w.WriteHeader(http.StatusNotFound)
			return
		}
		// packages: api/packages/<owner>/generic/<pkg>/<version>/<file>
		if strings.HasPrefix(p, "api/packages/") {
			seg := strings.Split(p, "/")
			if len(seg) >= 6 && f.packages[seg[4]+"/"+seg[5]] && !f.downloadGone[seg[4]+"/"+seg[5]] {
				w.WriteHeader(http.StatusOK)
				return
			}
			w.WriteHeader(http.StatusNotFound)
			return
		}
		w.WriteHeader(http.StatusNotFound)
	}))
	t.Cleanup(srv.Close)
	return gitea.New(srv.URL, "admin", "", "")
}

// healthy is the registry as it should be: agent tagged + published, golden published.
func healthy() *fakeRegistry {
	return &fakeRegistry{
		tagFiles: map[string]bool{"felhom-agent/v0.128.0/configs/felhom-mkfs-guarded.sh": true},
		packages: map[string]bool{"felhom-agent/0.128.0": true, "felhom-golden/0.210.0": true},
	}
}

func saveArtifacts(t *testing.T, s *Server) *httptest.ResponseRecorder {
	t.Helper()
	form := url.Values{
		"agent_version":  {"0.128.0"},
		"golden_version": {"0.210.0"},
		"agent_sha256":   {strings.Repeat("a", 64)},
		"golden_sha256":  {strings.Repeat("b", 64)},
	}
	r := httptest.NewRequest(http.MethodPost, "/configuration/artifacts", strings.NewReader(form.Encode()))
	r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	w := httptest.NewRecorder()
	s.handleSetArtifacts(w, r)
	return w
}

func flashOf(w *httptest.ResponseRecorder) string {
	loc := w.Header().Get("Location")
	if i := strings.Index(loc, "flash="); i >= 0 {
		return loc[i+len("flash="):]
	}
	return ""
}

func assertUnchanged(t *testing.T, st *store.Store, why string) {
	t.Helper()
	if m := st.GetArtifactManifest(); m.AgentVersion != "" || m.GoldenVersion != "" {
		t.Fatalf("%s: manifest was WRITTEN anyway: %+v", why, m)
	}
}

// A — the tag is missing and the package is there. THE ONE THAT MATTERS: this is 2026-08-09 exactly.
// WRONG OUTCOME: saved, and every install dies five steps in, as root, on a virgin machine.
func TestInstallability_A_TagMissing(t *testing.T) {
	s, st := newTestServer(t)
	fr := healthy()
	fr.tagFiles = map[string]bool{} // published, never tagged
	s.SetGiteaClient(fr.start(t))

	w := saveArtifacts(t, s)
	if got := flashOf(w); got != "artifact_tag_missing" {
		t.Fatalf("flash = %q, want artifact_tag_missing — Friday's defect is not being caught", got)
	}
	assertUnchanged(t, st, "A")
}

// B — the tag is there and the package was pruned out from under it (R-287).
// WRONG OUTCOME: saved, and the installer 404s on the binary itself.
func TestInstallability_B_PackagePruned(t *testing.T) {
	s, st := newTestServer(t)
	fr := healthy()
	// R-287's real shape: metadata still answers, the FILE does not. Modelled with a separate
	// download map so the sha resolver succeeds and the gate's download leg is what convicts.
	fr.downloadGone = map[string]bool{"felhom-agent/0.128.0": true}
	s.SetGiteaClient(fr.start(t))

	w := saveArtifacts(t, s)
	if got := flashOf(w); got != "artifact_pkg_missing" {
		t.Fatalf("flash = %q, want artifact_pkg_missing", got)
	}
	assertUnchanged(t, st, "B")
}

// C — both artifacts are installable, but the registry can produce no checksum for one of them.
// The manifest must not record a version with an empty sha: that is vouching bytes nobody verified.
//
// NOTE ON WHERE THIS IS ENFORCED, because the first draft of this test was wrong. With a Gitea
// client configured, resolveArtifactSHA fetches the sha AUTHORITATIVELY and ignores anything
// submitted — so a blank field in the form cannot produce a blank stored sha, and the scenario as
// originally written ("submit an empty sha") modelled nothing real. The genuine shape is Gitea
// answering with no sha256, and the EXISTING refusal owns it. This test pins the guarantee rather
// than the mechanism: whatever refuses, the manifest must be unchanged.
// WRONG OUTCOME: the hub vouches a checksum for bytes it never saw.
func TestInstallability_C_NoChecksum(t *testing.T) {
	s, st := newTestServer(t)
	fr := healthy()
	fr.shaless = map[string]bool{"felhom-agent/0.128.0": true} // metadata answers, with no sha256
	s.SetGiteaClient(fr.start(t))

	w := saveArtifacts(t, s)
	if got := flashOf(w); got == "artifacts_set" {
		t.Fatalf("a version with no resolvable checksum SAVED — the hub vouched bytes nobody verified")
	}
	assertUnchanged(t, st, "C")
}

// D — everything correct. The gate must be invisible on a good approval.
// WRONG OUTCOME: a new obstacle in front of a correct operator action.
func TestInstallability_D_HappyPathStillSaves(t *testing.T) {
	s, st := newTestServer(t)
	s.SetGiteaClient(healthy().start(t))

	w := saveArtifacts(t, s)
	if got := flashOf(w); got != "artifacts_set" {
		t.Fatalf("flash = %q, want artifacts_set — a correct approval was blocked", got)
	}
	m := st.GetArtifactManifest()
	if m.AgentVersion != "0.128.0" || m.GoldenVersion != "0.210.0" {
		t.Fatalf("manifest not saved on the happy path: %+v", m)
	}
}

// E — the registry cannot answer. It must refuse, and it must say "could not verify" rather than
// "missing": those are different facts and the operator acts differently on each.
// WRONG OUTCOME: saved with a warning. A warning beside a success is read as a success.
func TestInstallability_E_RegistryUnreachable(t *testing.T) {
	s, st := newTestServer(t)
	fr := healthy()
	fr.status = http.StatusBadGateway
	s.SetGiteaClient(fr.start(t))

	w := saveArtifacts(t, s)
	got := flashOf(w)
	if got == "artifacts_set" {
		t.Fatalf("saved while the registry was unreachable — the warning-beside-a-success shape")
	}
	if got != "artifact_unverifiable" {
		t.Fatalf("flash = %q, want artifact_unverifiable — an undetermined result must not be "+
			"reported as a missing artifact", got)
	}
	assertUnchanged(t, st, "E")
}

// The gate must not fire when there is no registry client at all (the legacy manual path), or a
// hub with Gitea unconfigured could never vouch anything.
func TestInstallability_NoGiteaClient_StillSaves(t *testing.T) {
	s, st := newTestServer(t)
	w := saveArtifacts(t, s)
	if got := flashOf(w); got != "artifacts_set" {
		t.Fatalf("flash = %q, want artifacts_set with no Gitea client configured", got)
	}
	if st.GetArtifactManifest().AgentVersion != "0.128.0" {
		t.Fatal("manifest not saved on the no-client path")
	}
}

// The probe must assert the path the INSTALLER fetches. A gate that probes some other path that
// merely exists is how 2026-08-09 stayed invisible.
func TestInstallability_ProbesTheInstallersOwnPath(t *testing.T) {
	if installerProbeConfig != "configs/felhom-mkfs-guarded.sh" {
		t.Fatalf("probe path = %q; it must be the FIRST config felhom-host-install.sh fetches "+
			"(step 5/8), which is the file whose 404 broke every install on 2026-08-09",
			installerProbeConfig)
	}
}
