package report

import (
	"encoding/json"
	"regexp"
	"strings"
	"testing"

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

// secretNameRe matches any JSON key that smells like a credential — mirrors the agent half.
var secretNameRe = regexp.MustCompile(`(?i)(password|secret|token|hash|passphrase|api[_-]?key|\bkey\b|enc:)`)

// rommCompose is a realistic catalog compose: user-data binds via ${USERDATA_PATH}/${HDD_PATH}, plus a
// secret-laden environment section (which must NEVER reach the recipe — bindings come from volumes only).
const rommCompose = `services:
  romm:
    image: romm:latest
    environment:
      - DB_PASSWORD=${DB_PASSWORD}
      - IGDB_CLIENT_SECRET=${IGDB_CLIENT_SECRET}
    volumes:
      - ${USERDATA_PATH}/roms:/roms
      - ${HDD_PATH}/appdata/romm/resources:/romm/resources
      - romm_redis_data:/data
volumes:
  romm_redis_data:
`

// secretLadenStack builds a romm Stack whose persisted Env carries synthetic secrets (an ENC: value and
// a token-shaped value) — exactly what the recipe must keep out.
func secretLadenStack() stacks.Stack {
	return stacks.Stack{
		Name:        "romm",
		Meta:        stacks.Metadata{Slug: "romm", DisplayName: "RomM"},
		ComposePath: "/stacks/romm/docker-compose.yml",
		Deployed:    true,
		AppConfig: &stacks.AppConfig{
			Deployed: true,
			Env: map[string]string{
				"HDD_PATH":           "/mnt/felhom-drives/felhom-flash",
				"DB_PASSWORD":        "ENC:U2FsdGVkX1+DEADBEEFsecret==",
				"IGDB_CLIENT_SECRET": "tok_live_SUPERSECRET_must_not_leak",
				"SECRET_KEY":         "ENC:another_encrypted_blob",
			},
		},
	}
}

// TestBuildAppRecipe_NoSecrets is THE load-bearing boundary test: the emitted AppRecipe for an app whose
// deploy config contains secrets (ENC: + token-shaped) carries NONE of those values and NO
// credential-shaped key — only the {catalog_ref, enabled, storage_bindings} allowlist.
func TestBuildAppRecipe_NoSecrets(t *testing.T) {
	s := secretLadenStack()
	rec := buildAppRecipe(s, rommCompose)

	b, err := json.Marshal(rec)
	if err != nil {
		t.Fatal(err)
	}
	out := string(b)

	// (1) none of the secret VALUES survived.
	for _, leak := range []string{"ENC:", "U2FsdGVkX1", "DEADBEEF", "tok_live_SUPERSECRET_must_not_leak", "another_encrypted_blob"} {
		if strings.Contains(out, leak) {
			t.Errorf("SECRET LEAK: emitted recipe contains %q\n recipe: %s", leak, out)
		}
	}
	// (2) no credential-shaped KEY survived (DB_PASSWORD / SECRET_KEY / IGDB_CLIENT_SECRET as keys).
	assertNoSecretKeys(t, b)

	// (3) positive: we DID emit the allowlisted facts (not a vacuous pass).
	if rec.CatalogRef != "romm" || !rec.Enabled {
		t.Errorf("expected catalog_ref=romm enabled=true, got %+v", rec)
	}
	if len(rec.StorageBindings) != 2 {
		t.Fatalf("expected 2 storage bindings (roms + resources), got %+v", rec.StorageBindings)
	}
}

// TestBuildAppRecipe_AllowlistIsLoadBearing is the companion (red-proof of the boundary test): a NAIVE
// emitter that dumps AppConfig.Env (i.e. the allowlist guard removed) WOULD leak the token — proving the
// no-secrets assertion above is real, not vacuous. The production emitter must NOT leak it.
func TestBuildAppRecipe_AllowlistIsLoadBearing(t *testing.T) {
	s := secretLadenStack()
	const token = "tok_live_SUPERSECRET_must_not_leak"

	// The "guard removed" shape — dumping Env alongside the app. This is what the boundary forbids.
	unsafe, _ := json.Marshal(map[string]any{"catalog_ref": s.Meta.Slug, "env": s.AppConfig.Env})
	if !strings.Contains(string(unsafe), token) {
		t.Fatal("companion is broken: the unsafe (guard-removed) shape should contain the secret token")
	}

	// The REAL emitter must keep it out — same fixture, allowlist intact.
	real, _ := json.Marshal(buildAppRecipe(s, rommCompose))
	if strings.Contains(string(real), token) {
		t.Fatalf("BOUNDARY VIOLATION: production emitter leaked the token: %s", real)
	}
}

// Scenario E (fork-4) — the OffsiteRestic DR coord carries coordinates ONLY (no password/key), clears the
// secret-name regex, and its fields are emitted. Extends the _NoSecrets boundary to the new field.
func TestDRResticCoord_NoSecrets(t *testing.T) {
	half := &DRRecipeAppHalf{
		RecipeVersion: DRRecipeVersion,
		Customer:      DRCustomer{ID: "cust", Display: "Cust", Domain: "demo-felhom.eu"},
		Apps:          []AppRecipe{},
		OffsiteRestic: &DRResticCoord{
			Host: "u629193-sub1.your-storagebox.de", User: "u629193-sub1", Port: 23, RepoPath: "/home/felhom-demo-repo",
		},
	}
	b, err := json.Marshal(half)
	if err != nil {
		t.Fatal(err)
	}
	// (1) no credential-shaped KEY name survived (the DRResticCoord field names must clear the regex).
	assertNoSecretKeys(t, b)
	out := string(b)
	// (2) positive: the coordinate fields ARE emitted (not a vacuous pass).
	for _, want := range []string{`"offsite_restic"`, `"host":"u629193-sub1.your-storagebox.de"`, `"user":"u629193-sub1"`, `"port":23`, `"repo_path":"/home/felhom-demo-repo"`} {
		if !strings.Contains(out, want) {
			t.Errorf("DR coord missing %s in %s", want, out)
		}
	}
	// (3) the password/key field names must never appear.
	if strings.Contains(out, "repo_password") || strings.Contains(out, "ssh_key") || strings.Contains(out, "password") {
		t.Fatalf("DR coord must carry NO password/key: %s", out)
	}
}

func TestAppStorageBindings(t *testing.T) {
	got := appStorageBindings(rommCompose, "/mnt/felhom-drives/felhom-flash")
	want := map[string]StorageBinding{
		"/roms":           {ContainerPath: "/roms", Drive: "felhom-flash", Subpath: "userdata/roms"},
		"/romm/resources": {ContainerPath: "/romm/resources", Drive: "felhom-flash", Subpath: "appdata/romm/resources"},
	}
	if len(got) != len(want) {
		t.Fatalf("got %d bindings, want %d: %+v", len(got), len(want), got)
	}
	for _, b := range got {
		w, ok := want[b.ContainerPath]
		if !ok || b != w {
			t.Errorf("binding %+v unexpected (want %+v)", b, w)
		}
	}
	// The named volume (romm_redis_data) is NOT a drive bind → excluded.
	for _, b := range got {
		if strings.Contains(b.Subpath, "redis") {
			t.Errorf("named volume leaked into bindings: %+v", b)
		}
	}
}

// TestAppStorageBindings_NoHDD: an app with no HDD_PATH (rootfs-only) yields no bindings, non-nil slice.
func TestAppStorageBindings_NoHDD(t *testing.T) {
	got := appStorageBindings(rommCompose, "")
	if got == nil || len(got) != 0 {
		t.Errorf("no HDD_PATH should yield empty (non-nil) bindings, got %+v", got)
	}
}

func TestBuildDRRecipeAppHalf(t *testing.T) {
	reader := func(path string) string {
		if path == "/stacks/romm/docker-compose.yml" {
			return rommCompose
		}
		return ""
	}
	all := []stacks.Stack{
		secretLadenStack(),
		{Name: "traefik", Protected: true, Deployed: true},                                  // protected → excluded
		{Name: "vikunja", Meta: stacks.Metadata{Slug: "vikunja"}, Deployed: false},          // not deployed → excluded
		{Name: "actualbudget", Meta: stacks.Metadata{Slug: "actualbudget"}, Deployed: true}, // rootfs app, no compose path
	}
	half := BuildDRRecipeAppHalf("cust-demo", "Demo Customer", "demo-felhom.eu", all, reader)

	if half.RecipeVersion != 1 {
		t.Errorf("recipe_version=%d want 1", half.RecipeVersion)
	}
	if half.Customer.ID != "cust-demo" || half.Customer.Display != "Demo Customer" || half.Customer.Domain != "demo-felhom.eu" {
		t.Errorf("customer = %+v", half.Customer)
	}
	// Only romm + actualbudget (deployed, non-protected). traefik (protected) + vikunja (not deployed) out.
	if len(half.Apps) != 2 {
		t.Fatalf("expected 2 apps, got %d: %+v", len(half.Apps), half.Apps)
	}
	byRef := map[string]AppRecipe{}
	for _, a := range half.Apps {
		byRef[a.CatalogRef] = a
	}
	if r, ok := byRef["romm"]; !ok || len(r.StorageBindings) != 2 {
		t.Errorf("romm recipe wrong: %+v", r)
	}
	if r, ok := byRef["actualbudget"]; !ok || len(r.StorageBindings) != 0 {
		t.Errorf("actualbudget (rootfs) should have 0 bindings: %+v", r)
	}

	// Whole-half no-secrets sweep.
	b, _ := json.Marshal(half)
	assertNoSecretKeys(t, b)
	if strings.Contains(string(b), "tok_live_SUPERSECRET_must_not_leak") {
		t.Errorf("SECRET LEAK in assembled app-half: %s", b)
	}
}

// assertNoSecretKeys walks decoded JSON and fails on any object key matching secretNameRe.
func assertNoSecretKeys(t *testing.T, jsonBytes []byte) {
	t.Helper()
	var v any
	if err := json.Unmarshal(jsonBytes, &v); err != nil {
		t.Fatal(err)
	}
	var walk func(prefix string, node any)
	walk = func(prefix string, node any) {
		switch n := node.(type) {
		case map[string]any:
			for k, child := range n {
				if secretNameRe.MatchString(k) {
					t.Errorf("secret-shaped key %q at %s — the recipe must carry no credential field", k, prefix)
				}
				walk(prefix+"."+k, child)
			}
		case []any:
			for _, child := range n {
				walk(prefix, child)
			}
		}
	}
	walk("<root>", v)
}
