package stacks

import (
	"os"
	"path/filepath"
	"testing"
)

// TestLifecycleParsing proves the catalog `lifecycle:` field flows through .felhom.yml parsing into
// the three predicates the rest of the system branches on. The sync round-trip is the same path a
// real catalog push takes: .felhom.yml on disk → LoadMetadata → state visible.
func TestLifecycleParsing(t *testing.T) {
	cases := []struct {
		name       string
		yml        string
		wantEff    string
		canInstall bool
		abandoned  bool
	}{
		{"absent field is available", "display_name: X\n", LifecycleAvailable, true, false},
		{"explicit available", "display_name: X\nlifecycle: available\n", LifecycleAvailable, true, false},
		{"empty value is available", "display_name: X\nlifecycle: \"\"\n", LifecycleAvailable, true, false},
		{"hidden", "display_name: X\nlifecycle: hidden\n", LifecycleHidden, false, false},
		{"abandoned", "display_name: X\nlifecycle: abandoned\n", LifecycleAbandoned, false, true},
		{"quoted abandoned", "display_name: X\nlifecycle: \"abandoned\"\n", LifecycleAbandoned, false, true},
		// A typo in a catalog push must NOT brick the template. Fail-OPEN here is deliberate and is
		// the opposite of the deploy gate's posture: an unknown state most likely means the catalog
		// is newer than this controller, and silently pulling a working app out of every customer's
		// catalog is the worse failure.
		{"unknown value degrades to available", "display_name: X\nlifecycle: retired\n", LifecycleAvailable, true, false},
		{"case-sensitive: Abandoned is unknown", "display_name: X\nlifecycle: Abandoned\n", LifecycleAvailable, true, false},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			dir := t.TempDir()
			if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte(c.yml), 0644); err != nil {
				t.Fatal(err)
			}
			meta := LoadMetadata(dir)
			if got := meta.EffectiveLifecycle(); got != c.wantEff {
				t.Errorf("EffectiveLifecycle() = %q, want %q", got, c.wantEff)
			}
			if got := meta.CanInstall(); got != c.canInstall {
				t.Errorf("CanInstall() = %v, want %v", got, c.canInstall)
			}
			if got := meta.IsAbandoned(); got != c.abandoned {
				t.Errorf("IsAbandoned() = %v, want %v", got, c.abandoned)
			}
		})
	}
}

// TestLifecycleDoesNotDisturbOtherMetadata: the new field must not change how anything else parses.
func TestLifecycleDoesNotDisturbOtherMetadata(t *testing.T) {
	dir := t.TempDir()
	yml := `display_name: "Plant-it"
slug: plant-it
category: home
lifecycle: abandoned
resources:
  mem_limit: "256M"
`
	if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte(yml), 0644); err != nil {
		t.Fatal(err)
	}
	meta := LoadMetadata(dir)
	if meta.DisplayName != "Plant-it" || meta.Slug != "plant-it" || meta.Category != "home" {
		t.Fatalf("sibling fields damaged: %+v", meta)
	}
	if meta.Resources.MemLimit != "256M" {
		t.Errorf("MemLimit = %q, want 256M", meta.Resources.MemLimit)
	}
	if !meta.IsAbandoned() {
		t.Error("lifecycle lost")
	}
}

// TestDeployRefusesNonAvailable is the manager-level half of the fail-closed deploy gate.
//
// COMPANION RED-PROOF: delete the `if !meta.CanInstall()` block in DeployStack and this test fails —
// the deploy proceeds past the gate on an abandoned fixture. Recorded in REPORT.md.
func TestDeployRefusesNonAvailable(t *testing.T) {
	for _, lc := range []string{LifecycleHidden, LifecycleAbandoned} {
		t.Run(lc, func(t *testing.T) {
			dir := t.TempDir()
			if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"),
				[]byte("display_name: X\nlifecycle: "+lc+"\n"), 0644); err != nil {
				t.Fatal(err)
			}
			meta := LoadMetadata(dir)
			if meta.CanInstall() {
				t.Fatalf("%s must not be installable — this is the predicate the deploy gate reads", lc)
			}
		})
	}
}
