package backup

import (
	"encoding/json"
	"io"
	"log"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"testing"
	"time"

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

// --- shared harness -------------------------------------------------------------------------------

// sharesEnv is a Manager wired for the R-7b shares source: a real settings store, real temp drives,
// and FAKE copy/exec seams so nothing shells out to rsync, du, docker or restic.
type sharesEnv struct {
	m *Manager
	// mirrored records every tier2Mirror call as "src=>dst" in call order.
	mirrored []string
	// drives maps a label to the temp dir standing in for that registered drive.
	drives map[string]string
	sett   *settings.Settings
	tmp    string
}

// newSharesEnv registers the named drives, turns sharing on, and installs the fake seams. Shares are
// added afterwards with addShare so each test states exactly the registry it needs.
func newSharesEnv(t *testing.T, driveNames ...string) *sharesEnv {
	t.Helper()
	tmp := t.TempDir()
	sett, err := settings.Load(filepath.Join(tmp, "settings.json"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatal(err)
	}
	env := &sharesEnv{drives: map[string]string{}, sett: sett, tmp: tmp}
	for _, name := range driveNames {
		p := filepath.Join(tmp, name)
		if err := os.MkdirAll(p, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := sett.AddStoragePath(settings.StoragePath{Path: p, Label: name, Schedulable: true}); err != nil {
			t.Fatal(err)
		}
		env.drives[name] = p
	}
	if err := sett.SetSMBEnabled(true); err != nil {
		t.Fatal(err)
	}
	cfg := &config.Config{}
	cfg.Paths.DataDir = filepath.Join(tmp, "data")
	sysDrive := filepath.Join(tmp, "sysdrive")
	if err := os.MkdirAll(sysDrive, 0o755); err != nil {
		t.Fatal(err)
	}
	cfg.Paths.SystemDataPath = sysDrive

	env.m = &Manager{
		cfg: cfg, settings: sett, logger: log.New(io.Discard, "", 0), systemDataPath: sysDrive,
		// Fake mirror: records the call AND actually copies, so restore tests have real bytes.
		tier2Mirror: func(src, dst string) error {
			env.mirrored = append(env.mirrored, src+"=>"+dst)
			return copyTreeForTest(src, dst)
		},
		sharesPassdbCapture: func() ([]byte, error) { return []byte("FAKE-PASSDB-TAR"), nil },
		// Each drive dir is its own "device" — otherwise hdd_1/hdd_2 share one st_dev here and the
		// off-drive guard refuses the target, so the mirror under test never runs.
		samePhysicalDevice: oneDrivePerSubtree,
	}
	return env
}

// addShare registers a share, creating its folder with one file so it is "available".
func (e *sharesEnv) addShare(t *testing.T, drive, name string, offsite bool) string {
	t.Helper()
	p := filepath.Join(e.drives[drive], name)
	if err := os.MkdirAll(p, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(p, name+".txt"), []byte("content-of-"+name), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := e.sett.AddSMBShare(settings.SMBShare{Name: name, Path: p, Offsite: offsite, CreatedAt: "2026-07-18T00:00:00Z"}); err != nil {
		t.Fatal(err)
	}
	return p
}

// copyTreeForTest is a minimal recursive copy standing in for rsync in the mirror seam.
func copyTreeForTest(src, dst string) error {
	fi, err := os.Stat(src)
	if err != nil {
		return err
	}
	if !fi.IsDir() {
		b, rErr := os.ReadFile(src)
		if rErr != nil {
			return rErr
		}
		if mErr := os.MkdirAll(filepath.Dir(dst), 0o755); mErr != nil {
			return mErr
		}
		return os.WriteFile(dst, b, 0o644)
	}
	if err := os.MkdirAll(dst, 0o755); err != nil {
		return err
	}
	entries, err := os.ReadDir(src)
	if err != nil {
		return err
	}
	for _, e := range entries {
		if err := copyTreeForTest(filepath.Join(src, e.Name()), filepath.Join(dst, e.Name())); err != nil {
			return err
		}
	}
	return nil
}

// mirroredDsts returns just the destination side of every recorded mirror call.
func (e *sharesEnv) mirroredDsts() []string {
	var out []string
	for _, s := range e.mirrored {
		out = append(out, s[strings.Index(s, "=>")+2:])
	}
	return out
}

// --- Part 1: payload ------------------------------------------------------------------------------

// The manifest must carry the share DEFINITIONS verbatim — that is the whole point of the payload:
// after a DR rebuild the customer gets their files AND their „Megosztás" page back, not an empty one.
func TestSharesPayloadManifestMatchesRegistry(t *testing.T) {
	env := newSharesEnv(t, "hdd_1")
	env.addShare(t, "hdd_1", "dokumentumok", true)
	env.addShare(t, "hdd_1", "filmek", false)

	dir, passdbOK, err := env.m.buildSharesPayload()
	if err != nil {
		t.Fatalf("buildSharesPayload: %v", err)
	}
	if !passdbOK {
		t.Fatal("expected the (faked) passdb capture to succeed")
	}
	blob, err := os.ReadFile(filepath.Join(dir, sharesManifestName))
	if err != nil {
		t.Fatalf("read manifest: %v", err)
	}
	var mf SharesManifest
	if err := json.Unmarshal(blob, &mf); err != nil {
		t.Fatalf("manifest is not valid JSON: %v", err)
	}
	if mf.Version != sharesManifestVersion {
		t.Errorf("manifest version = %d, want %d", mf.Version, sharesManifestVersion)
	}
	if mf.ServerName != settings.DefaultSMBServerName {
		t.Errorf("server name = %q, want %q", mf.ServerName, settings.DefaultSMBServerName)
	}
	want := env.sett.GetSMBShares()
	sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name })
	if len(mf.Shares) != len(want) {
		t.Fatalf("manifest has %d shares, registry has %d", len(mf.Shares), len(want))
	}
	for i := range want {
		if mf.Shares[i] != want[i] {
			t.Errorf("share %d: manifest %+v != registry %+v", i, mf.Shares[i], want[i])
		}
	}
	// The credential copy must be present and 0600 — it is secret-bearing.
	st, err := os.Stat(filepath.Join(dir, sharesPassdbName))
	if err != nil {
		t.Fatalf("passdb archive missing: %v", err)
	}
	if runtimeIsPOSIX() && st.Mode().Perm() != 0o600 {
		t.Errorf("passdb archive mode = %o, want 600 (secret-bearing)", st.Mode().Perm())
	}
}

// Determinism: an unchanged registry must marshal to IDENTICAL bytes regardless of the order shares
// were added in, so a no-op nightly run gives the tier-2 mirror nothing to rewrite.
func TestSharesPayloadManifestIsDeterministic(t *testing.T) {
	// Built from the registry only (no filesystem), so the two runs differ ONLY in insertion order —
	// otherwise the temp-dir paths baked into each share would make any comparison meaningless.
	read := func(order []string) []byte {
		sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
		if err != nil {
			t.Fatal(err)
		}
		for _, n := range order {
			if err := sett.AddSMBShare(settings.SMBShare{
				Name: n, Path: "/mnt/hdd_1/" + n, Offsite: true, CreatedAt: "2026-07-18T00:00:00Z",
			}); err != nil {
				t.Fatal(err)
			}
		}
		m := &Manager{settings: sett, logger: log.New(io.Discard, "", 0)}
		b, err := json.MarshalIndent(m.buildSharesManifest(), "", "  ")
		if err != nil {
			t.Fatal(err)
		}
		return b
	}
	a := read([]string{"alpha", "zulu", "mike"})
	b := read([]string{"zulu", "mike", "alpha"})
	if string(a) != string(b) {
		t.Errorf("manifest is order-dependent:\n%s\n---\n%s", a, b)
	}
}

// Scenario: samba container down. The payload must still be produced (manifest-only) and must NOT
// error — re-setting the SMB password is cheap; losing the definitions is not.
func TestSharesPayloadPassdbAbsentIsManifestOnly(t *testing.T) {
	env := newSharesEnv(t, "hdd_1")
	env.addShare(t, "hdd_1", "dokumentumok", true)
	env.m.sharesPassdbCapture = func() ([]byte, error) { return nil, os.ErrNotExist }

	dir, passdbOK, err := env.m.buildSharesPayload()
	if err != nil {
		t.Fatalf("a down container must not fail the payload: %v", err)
	}
	if passdbOK {
		t.Error("passdbOK must be false when the capture failed and no prior copy exists")
	}
	if _, err := os.Stat(filepath.Join(dir, sharesManifestName)); err != nil {
		t.Errorf("manifest must still be written: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, sharesPassdbName)); !os.IsNotExist(err) {
		t.Error("no passdb archive should exist when the capture failed and none was staged before")
	}
}

// A previously captured credential copy must SURVIVE a later failed capture — a stale credential is
// strictly better for DR than none, and the manifest carries no password of its own to contradict it.
func TestSharesPayloadKeepsPriorPassdbOnCaptureFailure(t *testing.T) {
	env := newSharesEnv(t, "hdd_1")
	env.addShare(t, "hdd_1", "dokumentumok", true)
	dir, _, err := env.m.buildSharesPayload()
	if err != nil {
		t.Fatal(err)
	}
	env.m.sharesPassdbCapture = func() ([]byte, error) { return nil, os.ErrNotExist }
	_, passdbOK, err := env.m.buildSharesPayload()
	if err != nil {
		t.Fatal(err)
	}
	if !passdbOK {
		t.Error("a previously staged passdb copy must keep passdbOK true")
	}
	b, err := os.ReadFile(filepath.Join(dir, sharesPassdbName))
	if err != nil || string(b) != "FAKE-PASSDB-TAR" {
		t.Errorf("prior passdb copy was lost: %q, %v", b, err)
	}
}

// The reserved key must never reach a Hungarian surface raw.
func TestDisplayStackNameMapsReservedKey(t *testing.T) {
	if got := DisplayStackName(SharesPseudoStack); got != SharesDisplayName {
		t.Errorf("DisplayStackName(%q) = %q, want %q", SharesPseudoStack, got, SharesDisplayName)
	}
	if got := DisplayStackName("immich"); got != "immich" {
		t.Errorf("DisplayStackName must be identity for ordinary stacks, got %q", got)
	}
}

// --- Part 2: tier-2 shares job --------------------------------------------------------------------

// Scenario B: an OPTIONAL (Felhőmentés-off) share is tier-2'd like any other — the tier-2 tier is the
// local copy every share gets; only the offsite tier discriminates.
func TestSharesTier2MirrorsBothClasses(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	env.addShare(t, "hdd_1", "dokumentumok", true)
	env.addShare(t, "hdd_1", "filmek", false)

	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatalf("RunSharesTier2: %v", err)
	}
	key := sharesDriveKey(env.drives["hdd_1"])
	destBase := filepath.Join(NamespaceRoot(env.drives["hdd_2"], true), "backups", "secondary", SharesPseudoStack)

	for _, share := range []string{"dokumentumok", "filmek"} {
		want := filepath.Join(destBase, key, share)
		if _, err := os.Stat(filepath.Join(want, share+".txt")); err != nil {
			t.Errorf("share %s was not mirrored to %s: %v", share, want, err)
		}
	}
	// The payload rides the target so it is independently restorable.
	if _, err := os.Stat(filepath.Join(destBase, sharesPayloadDestRel, sharesManifestName)); err != nil {
		t.Errorf("payload manifest missing from the tier-2 target: %v", err)
	}
	// Marker LAST: its presence means every leg + reconcile completed.
	b, err := os.ReadFile(filepath.Join(destBase, tier2LayoutMarker))
	if err != nil || string(b) != tier2LayoutVersion {
		t.Errorf("layout marker missing/wrong: %q, %v", b, err)
	}
	// Status is recorded under the reserved key, and renders as „Megosztások".
	cd := env.m.SharesTier2Status()
	if cd == nil || cd.LastStatus != "ok" {
		t.Fatalf("shares tier-2 status = %+v, want LastStatus=ok", cd)
	}
}

// The target may NEVER be the leg's own source drive — that would be a same-disk copy pretending to
// be a second backup. With shares on two drives each group must land on the other one.
func TestSharesTier2NeverTargetsItsOwnSourceDrive(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	env.addShare(t, "hdd_1", "egy", true)
	env.addShare(t, "hdd_2", "ketto", true)

	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatalf("RunSharesTier2: %v", err)
	}
	for _, call := range env.mirrored {
		src, dst, _ := strings.Cut(call, "=>")
		if strings.HasPrefix(src, env.drives["hdd_1"]) && strings.HasPrefix(dst, env.drives["hdd_1"]) {
			t.Errorf("leg mirrored onto its own source drive: %s", call)
		}
		if strings.HasPrefix(src, env.drives["hdd_2"]) && strings.HasPrefix(dst, env.drives["hdd_2"]) {
			t.Errorf("leg mirrored onto its own source drive: %s", call)
		}
	}
}

// Scenario F: a share whose drive is marked away is skipped LOUDLY and its path reaches NO mirror
// argument — mirroring a dead mountpoint would copy an empty dir over a good backup.
func TestSharesTier2SkipsDeadMountAndContinues(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2", "hdd_3")
	env.addShare(t, "hdd_1", "elo", true)
	deadPath := env.addShare(t, "hdd_3", "halott", true)
	if err := env.sett.SetDisconnected(env.drives["hdd_3"], true, nil); err != nil {
		t.Fatal(err)
	}

	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatalf("the run must continue for healthy shares: %v", err)
	}
	for _, call := range env.mirrored {
		if strings.Contains(call, deadPath) {
			t.Errorf("dead-mount share reached a mirror call: %s", call)
		}
	}
	var sawLive bool
	for _, dst := range env.mirroredDsts() {
		if strings.HasSuffix(dst, string(filepath.Separator)+"elo") {
			sawLive = true
		}
	}
	if !sawLive {
		t.Error("the healthy share must still be mirrored")
	}
}

// Zero shares / sharing disabled: a clean no-op — no dest tree, no status record, no empty dirs.
func TestSharesTier2ZeroSharesIsCleanNoOp(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatalf("RunSharesTier2: %v", err)
	}
	if len(env.mirrored) != 0 {
		t.Errorf("expected no mirror calls, got %v", env.mirrored)
	}
	if cd := env.m.SharesTier2Status(); cd != nil {
		t.Errorf("expected no status record for a feature nobody uses, got %+v", cd)
	}
	destBase := filepath.Join(NamespaceRoot(env.drives["hdd_2"], true), "backups", "secondary", SharesPseudoStack)
	if _, err := os.Stat(destBase); !os.IsNotExist(err) {
		t.Errorf("expected no _shares dest tree, stat err = %v", err)
	}
}

// A share removed from the registry must stop occupying the secondary drive within ONE run — the
// reconcile pass prunes it, reusing the same destBase-bounded removal guard the app path uses.
func TestSharesTier2ReconcilePrunesRemovedShare(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	env.addShare(t, "hdd_1", "marad", true)
	env.addShare(t, "hdd_1", "torolt", true)
	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatal(err)
	}
	key := sharesDriveKey(env.drives["hdd_1"])
	destBase := filepath.Join(NamespaceRoot(env.drives["hdd_2"], true), "backups", "secondary", SharesPseudoStack)
	stale := filepath.Join(destBase, key, "torolt")
	if _, err := os.Stat(stale); err != nil {
		t.Fatalf("precondition: %s should exist after the first run: %v", stale, err)
	}

	if err := env.sett.RemoveSMBShare("torolt"); err != nil {
		t.Fatal(err)
	}
	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(stale); !os.IsNotExist(err) {
		t.Errorf("removed share's dest dir was not pruned: stat err = %v", err)
	}
	if _, err := os.Stat(filepath.Join(destBase, key, "marad")); err != nil {
		t.Errorf("surviving share was pruned by mistake: %v", err)
	}
}

// The tier-2 half of the B′ isolation proof: the shares job must write ONLY under
// backups/secondary/_shares. A per-app dest tree standing beside it must come out byte-for-byte
// untouched — same contents, same bytes — and no mirror call may target it. Red-proof: point the
// shares destBase at backups/secondary/<share> (dropping the _shares segment) and this fails.
func TestSharesTier2LeavesPerAppTreeUntouched(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	env.addShare(t, "hdd_1", "dokumentumok", true)

	// A pre-existing per-app tier-2 dest with a sentinel payload.
	appDest := filepath.Join(NamespaceRoot(env.drives["hdd_2"], true), "backups", "secondary", "immich")
	if err := os.MkdirAll(filepath.Join(appDest, "recovery-unit"), 0o755); err != nil {
		t.Fatal(err)
	}
	sentinel := filepath.Join(appDest, "recovery-unit", "manifest.json")
	if err := os.WriteFile(sentinel, []byte(`{"app":"immich"}`), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatal(err)
	}

	b, err := os.ReadFile(sentinel)
	if err != nil || string(b) != `{"app":"immich"}` {
		t.Errorf("B′ INVARIANT VIOLATED — the per-app tier-2 tree was modified: %q, %v", b, err)
	}
	for _, call := range env.mirrored {
		if strings.Contains(call, appDest) {
			t.Errorf("B′ INVARIANT VIOLATED — a shares mirror targeted the per-app dest: %s", call)
		}
	}
	// And everything it DID write lives under the reserved subtree.
	sharesRoot := filepath.Join(NamespaceRoot(env.drives["hdd_2"], true), "backups", "secondary", SharesPseudoStack)
	for _, dst := range env.mirroredDsts() {
		if !strings.HasPrefix(dst, sharesRoot+string(filepath.Separator)) {
			t.Errorf("shares job wrote outside its reserved subtree: %s", dst)
		}
	}
}

// Drive keys must be collision-free across drives that share a basename.
func TestSharesDriveKeyIsCollisionFree(t *testing.T) {
	if sharesDriveKey("/mnt/a/data") == sharesDriveKey("/mnt/b/data") {
		t.Error("drives with the same basename must not map to the same dest key")
	}
	for _, bad := range []string{"/", "\x00", " "} {
		if k := sharesDriveKey(bad); strings.ContainsAny(k, `/\`) {
			t.Errorf("sharesDriveKey(%q) = %q — must be a single safe path segment", bad, k)
		}
	}
}

// runtimeIsPOSIX reports whether file-mode assertions are meaningful on this host (Windows reports
// synthesised permissions, so mode checks there are noise rather than signal).
func runtimeIsPOSIX() bool { return os.PathSeparator == '/' }

// REGRESSION (found in live validation, not by a unit test): the first demo run pushed a hub event
// reading „Másodlagos mentés elkészült: _shares" — the reserved key reached Hungarian customer copy.
// The mapping now happens at the SOURCE of the notification rather than at each wiring site, so no
// future notifier wiring can reintroduce the leak. Red-proof: pass SharesPseudoStack to m.tier2Notify
// instead of DisplayStackName(...) and this fails.
func TestSharesTier2NotifierNeverLeaksReservedKey(t *testing.T) {
	env := newSharesEnv(t, "hdd_1", "hdd_2")
	env.addShare(t, "hdd_1", "dokumentumok", true)

	var notified []string
	env.m.tier2Notify = func(stackName, destLabel string, dur time.Duration, err error) {
		notified = append(notified, stackName)
	}
	if err := env.m.RunSharesTier2(); err != nil {
		t.Fatal(err)
	}
	if len(notified) == 0 {
		t.Fatal("precondition: the notifier should have fired")
	}
	for _, n := range notified {
		if n == SharesPseudoStack {
			t.Errorf("the reserved key reached the notification boundary raw: %q", n)
		}
		if n != SharesDisplayName {
			t.Errorf("notifier got %q, want %q", n, SharesDisplayName)
		}
	}
}
