package stacks

import (
	"log"
	"os"
	"path/filepath"
	"sync"
	"testing"

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

// newClassifyManager registers a single stack "app" whose .felhom.yml + docker-compose.yml are
// written to a temp dir, so Manager.ClassifiedBinds runs the REAL LoadMetadata → validate → classify
// path end-to-end (NO seams) — the F-S3 lesson: the wiring is where typos hide.
func newClassifyManager(t *testing.T, felhomYML, compose string) *Manager {
	t.Helper()
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, ".felhom.yml"), []byte(felhomYML), 0o644); err != nil {
		t.Fatal(err)
	}
	composePath := filepath.Join(dir, "docker-compose.yml")
	if err := os.WriteFile(composePath, []byte(compose), 0o644); err != nil {
		t.Fatal(err)
	}
	return &Manager{
		cfg:    &config.Config{},
		logger: log.New(os.Stderr, "", 0),
		mu:     sync.RWMutex{},
		stacks: map[string]*Stack{"app": {Name: "app", ComposePath: composePath}},
	}
}

func classSet(cbs []appbackup.ClassifiedBind) map[string]appbackup.BindClass {
	m := map[string]appbackup.BindClass{}
	for _, c := range cbs {
		m[string(c.Root)+"/"+c.RelPath] = c.Class
	}
	return m
}

// --- Group D: end-to-end wiring (NO seams) ---

// TestClassifiedBinds_ValidFixture (Group D, immich shape): a clean block resolves through the REAL
// LoadMetadata path to the expected explicit classes.
func TestClassifiedBinds_ValidFixture(t *testing.T) {
	felhom := `display_name: Immich
backup:
  hdd:
    - path: appdata/immich
      class: mandatory
  userdata:
    - path: media/photos
      class: optional
`
	compose := `services:
  immich:
    volumes:
      - ${HDD_PATH}/appdata/immich:/usr/src/app/upload
      - ${USERDATA_PATH}/media/photos:/external/photos:ro
`
	m := newClassifyManager(t, felhom, compose)
	cbs, has := m.ClassifiedBinds("app")
	if !has {
		t.Fatal("valid block must classify (hasClassification=true)")
	}
	got := classSet(cbs)
	if got["hdd/appdata/immich"] != appbackup.ClassMandatory {
		t.Errorf("appdata/immich = %q, want mandatory", got["hdd/appdata/immich"])
	}
	if got["userdata/media/photos"] != appbackup.ClassOptional {
		t.Errorf("media/photos = %q, want optional (explicit beats :ro default)", got["userdata/media/photos"])
	}
}

// TestClassifiedBinds_RejectedFixture (Group D + RP-4): a block whose entry matches no compose bind
// is rejected by the REAL LoadMetadata choke point → the app degrades to legacy (hasClassification
// false, no class semantics). Companion RP-4: if LoadMetadata accepts meta.Backup WITHOUT validating,
// this returns hasClassification=true and the assertion FAILS.
func TestClassifiedBinds_RejectedFixture(t *testing.T) {
	felhom := `display_name: Sonarr
backup:
  userdata:
    - path: media/tvv
      class: excluded
`
	compose := `services:
  sonarr:
    volumes:
      - ${USERDATA_PATH}/media/tv:/tv
`
	m := newClassifyManager(t, felhom, compose)
	cbs, has := m.ClassifiedBinds("app")
	if has {
		t.Fatal("a block with an unmatched path must be REJECTED → legacy (hasClassification=false)")
	}
	// The real bind is still returned, but as legacy (no class) — never silently shifted to a default.
	for _, c := range cbs {
		if c.Origin != appbackup.OriginLegacy || c.Class != "" {
			t.Errorf("rejected-block bind = %+v, want origin=legacy, empty class", c)
		}
	}
}

// TestClassifiedBinds_NoBlockLegacy: an app with no backup block classifies as legacy (inertness).
func TestClassifiedBinds_NoBlockLegacy(t *testing.T) {
	felhom := `display_name: Navidrome`
	compose := `services:
  nav:
    volumes:
      - ${USERDATA_PATH}/media/music:/music:ro
`
	m := newClassifyManager(t, felhom, compose)
	if _, has := m.ClassifiedBinds("app"); has {
		t.Error("no backup block → hasClassification must be false (legacy)")
	}
}

// --- Group E: catalog fidelity (≥3 real blocks) ---

// TestCatalogBlocks_Fidelity pins the exact class sets of representative Part-4 blocks (immich,
// paperless-ngx, sonarr) against representative composes — the same content committed to the catalog.
// The full 13-app proof is the Part-4 cross-check against the live catalog clone (§13 + REPORT).
func TestCatalogBlocks_Fidelity(t *testing.T) {
	cases := []struct {
		name    string
		felhom  string
		compose string
		want    map[string]appbackup.BindClass
	}{
		{
			name: "immich",
			felhom: `backup:
  hdd:
    - path: appdata/immich
      class: mandatory
  userdata:
    - path: media/photos
      class: optional
`,
			compose: `services:
  immich:
    volumes:
      - ${HDD_PATH}/appdata/immich:/usr/src/app/upload
      - ${USERDATA_PATH}/media/photos:/external/photos:ro
`,
			want: map[string]appbackup.BindClass{
				"hdd/appdata/immich":    appbackup.ClassMandatory,
				"userdata/media/photos": appbackup.ClassOptional,
			},
		},
		{
			name: "paperless-ngx",
			felhom: `backup:
  hdd:
    - path: appdata/paperless/media
      class: mandatory
    - path: appdata/paperless/export
      class: excluded
  userdata:
    - path: import/paperless
      class: excluded
`,
			compose: `services:
  webserver:
    volumes:
      - ${HDD_PATH}/appdata/paperless/media:/usr/src/paperless/media
      - ${HDD_PATH}/appdata/paperless/export:/usr/src/paperless/export
      - ${USERDATA_PATH}/import/paperless:/usr/src/paperless/consume
`,
			want: map[string]appbackup.BindClass{
				"hdd/appdata/paperless/media":  appbackup.ClassMandatory,
				"hdd/appdata/paperless/export": appbackup.ClassExcluded,
				"userdata/import/paperless":    appbackup.ClassExcluded,
			},
		},
		{
			name: "sonarr",
			felhom: `backup:
  userdata:
    - path: media/tv
      class: excluded
    - path: downloads
      class: excluded
`,
			compose: `services:
  sonarr:
    volumes:
      - ${USERDATA_PATH}/media/tv:/tv
      - ${USERDATA_PATH}/downloads:/downloads
`,
			want: map[string]appbackup.BindClass{
				"userdata/media/tv":  appbackup.ClassExcluded,
				"userdata/downloads": appbackup.ClassExcluded,
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			m := newClassifyManager(t, tc.felhom, tc.compose)
			cbs, has := m.ClassifiedBinds("app")
			if !has {
				t.Fatalf("%s block must classify clean", tc.name)
			}
			got := classSet(cbs)
			if len(got) != len(tc.want) {
				t.Errorf("%s: got %d binds, want %d: %v", tc.name, len(got), len(tc.want), got)
			}
			for k, want := range tc.want {
				if got[k] != want {
					t.Errorf("%s: %s = %q, want %q", tc.name, k, got[k], want)
				}
			}
		})
	}
}
