package web

import (
	"strings"
	"testing"

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

func lcStack(name, lifecycle string, deployed bool) stacks.Stack {
	return stacks.Stack{
		Name:     name,
		Deployed: deployed,
		State:    stacks.StateNotDeployed,
		Meta:     stacks.Metadata{DisplayName: name, Slug: name, Lifecycle: lifecycle},
	}
}

// TestVisibleCatalogStacks pins the catalog-listing rule on both axes at once: withdrawn templates
// disappear from the OFFER list, and a deployed instance of the very same template stays.
//
// COMPANION RED-PROOF: drop the `st.Deployed ||` clause and the two "deployed" rows fail — a
// customer's working app vanishes from their own Alkalmazások page, which is the failure that makes
// withdrawing an app dangerous in the first place.
func TestVisibleCatalogStacks(t *testing.T) {
	in := []stacks.Stack{
		lcStack("bookstack", "", false),            // available, not deployed → OFFERED
		lcStack("immich", "", true),                // available, deployed     → shown
		lcStack("plant-it", "abandoned", false),    // withdrawn, not deployed → HIDDEN
		lcStack("plant-it-run", "abandoned", true), // withdrawn, DEPLOYED     → shown
		lcStack("oldapp", "hidden", false),         // withdrawn, not deployed → HIDDEN
		lcStack("oldapp-run", "hidden", true),      // withdrawn, DEPLOYED     → shown
		lcStack("typoapp", "bogus", false),         // unknown → available → OFFERED (fail-open)
	}
	got := map[string]bool{}
	for _, st := range visibleCatalogStacks(in) {
		got[st.Name] = true
	}
	want := []string{"bookstack", "immich", "plant-it-run", "oldapp-run", "typoapp"}
	notWant := []string{"plant-it", "oldapp"}

	for _, n := range want {
		if !got[n] {
			t.Errorf("%q must be listed", n)
		}
	}
	for _, n := range notWant {
		if got[n] {
			t.Errorf("%q is withdrawn and not deployed — it must NOT be offered", n)
		}
	}
	if len(got) != len(want) {
		t.Errorf("listed %d stacks, want %d (%v)", len(got), len(want), got)
	}
}

// TestVisibleCatalogStacks_ProtectedAlwaysSurvives: infra stacks carry no catalog metadata and must
// never be filtered out by a rule about catalog apps.
func TestVisibleCatalogStacks_ProtectedAlwaysSurvives(t *testing.T) {
	st := lcStack("traefik", "hidden", false)
	st.Protected = true
	if len(visibleCatalogStacks([]stacks.Stack{st})) != 1 {
		t.Fatal("a protected infra stack must never be hidden by the lifecycle filter")
	}
}

func TestLifecycleBadge(t *testing.T) {
	cases := []struct {
		lifecycle string
		wantBadge bool
	}{
		{"", false},
		{"available", false},
		// hidden renders NOTHING on a deployed app: "we stopped offering this" is not a fact the
		// customer running it needs, and a badge implying something is wrong would be misleading.
		{"hidden", false},
		{"abandoned", true},
		{"bogus", false},
	}
	for _, c := range cases {
		b := lifecycleBadge(stacks.Metadata{Lifecycle: c.lifecycle})
		if (b != nil) != c.wantBadge {
			t.Errorf("lifecycle %q: badge=%v, want present=%v", c.lifecycle, b, c.wantBadge)
		}
		if b != nil {
			if b.Label != "Nem karbantartott" {
				t.Errorf("label = %q", b.Label)
			}
			if b.Title == "" {
				t.Error("a badge that is only a word is a riddle — it must carry an explanation")
			}
		}
	}
}

// TestAbandonedBadgeRendersOnCatalogCard renders the PRODUCTION stacks template.
//
// COMPANION RED-PROOF: remove the {{template "meta_badge" ...}} line from stacks.html and this
// fails — the badge is the only thing telling a customer the software is no longer maintained.
func TestAbandonedBadgeRendersOnCatalogCard(t *testing.T) {
	deployed := lcStack("plant-it", "abandoned", true)
	deployed.State = stacks.StateRunning
	html := renderBackupPage(t, "stacks", map[string]interface{}{
		"Page": "stacks", "Title": "Alkalmazások",
		"Stacks":          []stacks.Stack{deployed},
		"MissingStorage":  map[string]string{},
		"NetworkWarnings": map[string]string{},
		"NetworkStubs":    map[string]string{},
		"StorageLabels":   map[string]string{},
		"Subdomains":      map[string]string{},
	})
	if !strings.Contains(html, "Nem karbantartott") {
		t.Fatal("a deployed abandoned app must carry the Nem karbantartott badge on its card")
	}
	if strings.Contains(html, "Telepítés</a>") {
		t.Error("an abandoned app must never offer a Telepítés button")
	}
}

// TestAvailableAppHasNoBadgeAndKeepsInstallButton is the negative half — without it, an impl that
// badges everything would pass the test above.
func TestAvailableAppHasNoBadgeAndKeepsInstallButton(t *testing.T) {
	html := renderBackupPage(t, "stacks", map[string]interface{}{
		"Page": "stacks", "Title": "Alkalmazások",
		"Stacks":          []stacks.Stack{lcStack("bookstack", "", false)},
		"MissingStorage":  map[string]string{},
		"NetworkWarnings": map[string]string{},
		"NetworkStubs":    map[string]string{},
		"StorageLabels":   map[string]string{},
		"Subdomains":      map[string]string{},
	})
	if strings.Contains(html, "Nem karbantartott") {
		t.Error("an available app must not be badged")
	}
	if !strings.Contains(html, "Telepítés</a>") {
		t.Fatal("an available, undeployed app must still offer Telepítés")
	}
}

// appInfoData mirrors appDetailHandler's data map — CRUCIALLY including `"Meta": st.Meta` as a
// stacks.Metadata VALUE inside a map[string]interface{}, which is what the handler really does.
// That detail is the entire point of this test: html/template cannot call a POINTER-receiver method
// on a non-addressable value, so `{{if .Meta.IsAbandoned}}` renders fine in a unit test that passes
// a *Metadata and 500s in production. Do not "simplify" this to a pointer.
func appInfoData(st stacks.Stack) map[string]interface{} {
	return map[string]interface{}{
		"Page": "stacks", "Title": st.Meta.DisplayName,
		"Stack": &st, "Meta": st.Meta, "AppInfo": st.Meta.AppInfo,
		"HasAppInfo": st.Meta.HasAppInfo(), "EffectiveSubdomain": st.Meta.Subdomain,
		"Domain": "demo-felhom.eu",
	}
}

// TestAppInfoRendersForEveryLifecycle is the regression test for a defect that SHIPPED: v0.158.0's
// first cut used pointer receivers, so `{{if .Meta.IsAbandoned}}` failed at RENDER time with
// "can't evaluate field IsAbandoned in type interface {}" — a 500 on EVERY app detail page,
// abandoned or not. It compiled, and every other test passed, because nothing rendered app_info.
//
// A template method call is only checked when the template runs. Render the page.
func TestAppInfoRendersForEveryLifecycle(t *testing.T) {
	for _, lc := range []string{"", "available", "hidden", "abandoned", "bogus"} {
		t.Run("lifecycle="+lc, func(t *testing.T) {
			st := lcStack("someapp", lc, true)
			st.State = stacks.StateRunning
			html := renderBackupPage(t, "app_info", appInfoData(st))
			if len(html) < 500 {
				t.Fatalf("app_info rendered only %d bytes — the page is broken", len(html))
			}
			wantNotice := lc == "abandoned"
			gotNotice := strings.Contains(html, "felhagyott a fejlesztéssel")
			if gotNotice != wantNotice {
				t.Errorf("abandoned notice present=%v, want %v (lifecycle %q)", gotNotice, wantNotice, lc)
			}
			gotBadge := strings.Contains(html, "Nem karbantartott")
			if gotBadge != wantNotice {
				t.Errorf("badge present=%v, want %v (lifecycle %q)", gotBadge, wantNotice, lc)
			}
		})
	}
}

// TestAppInfoHidesInstallButtonForWithdrawnApp — the undeployed view: a withdrawn app must not offer
// Telepítés, and an available one must.
func TestAppInfoHidesInstallButtonForWithdrawnApp(t *testing.T) {
	for _, c := range []struct {
		lifecycle string
		wantBtn   bool
	}{{"", true}, {"available", true}, {"hidden", false}, {"abandoned", false}} {
		st := lcStack("someapp", c.lifecycle, false)
		html := renderBackupPage(t, "app_info", appInfoData(st))
		got := strings.Contains(html, "Telepítés</a>")
		if got != c.wantBtn {
			t.Errorf("lifecycle %q: Telepítés button present=%v, want %v", c.lifecycle, got, c.wantBtn)
		}
	}
}
