package backup

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

// v0.147.0 slice 4a — the verification-copy listing/delete surface.
//
// DeleteOffsiteRestoreCopy is the ONLY delete this slice adds, so these tests are about what it must
// REFUSE as much as what it must do. Every refusal is asserted as a NON-EFFECT: the neighbouring copy
// and the customer's live data must still be on disk afterwards. A guard that returns an error but
// deletes anyway passes a naive test and loses data.
//
// RED-PROOF (run manually, confirmed): neutralise the isSafeStackName check in
// DeleteOffsiteRestoreCopy and TestDeleteVerifyCopyRefusesUnsafeNames fails hard — `stack: ""`
// resolves to the offsite-restore ROOT and os.RemoveAll takes every verification copy with it. That
// is the failure this guard exists to prevent, and it is data loss, not a bad error message.
//
// The HasPrefix containment check inside DeleteOffsiteRestoreCopy could NOT be red-proofed
// independently: with isSafeStackName in front of it, no input this API accepts can reach it with an
// escaping path, so removing it leaves every test green (and the variable unused). It is deliberate
// defence-in-depth against a future caller or a refactor that loosens the name check — kept, but
// honestly labelled here as unproven-by-test rather than pretending to a red-proof it does not have.

// verifyCopyEnv wires a manager with one drive and materialised verification copies.
type verifyCopyEnv struct {
	m     *Manager
	drive string
	root  string // <nsRoot>/backups/offsite-restore
}

func newVerifyCopyEnv(t *testing.T, copies ...string) *verifyCopyEnv {
	t.Helper()
	drive := t.TempDir()
	m, _, _ := classifiedOffboxManager(t, drive)
	root := m.offsiteRestoreRootFor(drive)
	for _, c := range copies {
		p := filepath.Join(root, c)
		if err := os.MkdirAll(p, 0o755); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(filepath.Join(p, "payload.bin"), []byte("restored bytes"), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	return &verifyCopyEnv{m: m, drive: drive, root: root}
}

func TestListOffsiteRestoreCopiesReportsPathAndSize(t *testing.T) {
	e := newVerifyCopyEnv(t, "immich", "nextcloud")

	got := e.m.ListOffsiteRestoreCopies()
	if len(got) != 2 {
		t.Fatalf("listed %d copies, want 2: %+v", len(got), got)
	}
	byStack := map[string]OffsiteRestoreCopy{}
	for _, c := range got {
		byStack[c.Stack] = c
	}
	for _, name := range []string{"immich", "nextcloud"} {
		c, ok := byStack[name]
		if !ok {
			t.Fatalf("%s missing from the listing", name)
		}
		// THE POINT of the listing: the customer could not previously see WHERE the copy was.
		want := filepath.Join(e.root, name)
		if c.Path != want {
			t.Errorf("%s path = %q, want %q", name, c.Path, want)
		}
		if c.SizeHuman == "" {
			t.Errorf("%s has no humanized size", name)
		}
		if c.Created.IsZero() {
			t.Errorf("%s has no creation time", name)
		}
	}
}

func TestListOffsiteRestoreCopiesEmptyIsNotAnError(t *testing.T) {
	// No offsite-restore directory at all — the normal state on a box that never ran a verification
	// restore. Must be an empty list, not a crash and not a phantom entry.
	e := newVerifyCopyEnv(t)
	if got := e.m.ListOffsiteRestoreCopies(); len(got) != 0 {
		t.Errorf("listed %d copies on a clean box, want 0: %+v", len(got), got)
	}
}

func TestDeleteVerifyCopyRemovesOnlyTheNamedOne(t *testing.T) {
	e := newVerifyCopyEnv(t, "immich", "nextcloud")
	// Live customer data next to the sandbox — must be untouched by any delete.
	live := filepath.Join(e.drive, "immich-live")
	if err := os.MkdirAll(live, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(live, "photo.jpg"), []byte("irreplaceable"), 0o644); err != nil {
		t.Fatal(err)
	}

	if err := e.m.DeleteOffsiteRestoreCopy("immich"); err != nil {
		t.Fatalf("delete: %v", err)
	}
	if _, err := os.Stat(filepath.Join(e.root, "immich")); !os.IsNotExist(err) {
		t.Error("the named copy survived the delete")
	}
	if _, err := os.Stat(filepath.Join(e.root, "nextcloud", "payload.bin")); err != nil {
		t.Errorf("a NEIGHBOURING copy was destroyed: %v", err)
	}
	if _, err := os.Stat(filepath.Join(live, "photo.jpg")); err != nil {
		t.Errorf("LIVE CUSTOMER DATA was destroyed: %v", err)
	}
}

func TestDeleteVerifyCopyRefusesUnsafeNames(t *testing.T) {
	e := newVerifyCopyEnv(t, "immich")
	// Something outside the sandbox that a traversal would reach.
	outside := filepath.Join(e.drive, "backups", "primary")
	if err := os.MkdirAll(outside, 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(outside, "unit.tar"), []byte("recovery unit"), 0o644); err != nil {
		t.Fatal(err)
	}

	for _, bad := range []string{
		"../primary",
		"../../..",
		"..",
		"immich/../../primary",
		"/etc",
		"",
	} {
		if err := e.m.DeleteOffsiteRestoreCopy(bad); err == nil {
			t.Errorf("delete(%q) was ACCEPTED — it must be refused", bad)
		}
		// The refusal must also be a NON-EFFECT.
		if _, err := os.Stat(filepath.Join(outside, "unit.tar")); err != nil {
			t.Fatalf("delete(%q) destroyed data outside the sandbox: %v", bad, err)
		}
		if _, err := os.Stat(filepath.Join(e.root, "immich", "payload.bin")); err != nil {
			t.Fatalf("delete(%q) destroyed an unrelated copy: %v", bad, err)
		}
	}
}

func TestDeleteVerifyCopyStaysInsideTheSandbox(t *testing.T) {
	e := newVerifyCopyEnv(t, "immich")
	// A well-formed name that simply does not exist must be an error, never a silent success that
	// could mask a path-resolution bug.
	if err := e.m.DeleteOffsiteRestoreCopy("no-such-app"); err == nil {
		t.Error("deleting a non-existent copy reported success")
	}
	// Everything under the sandbox root must resolve strictly inside it.
	for _, c := range e.m.ListOffsiteRestoreCopies() {
		rel, err := filepath.Rel(e.root, c.Path)
		if err != nil || rel == ".." || filepath.IsAbs(rel) || len(rel) > 2 && rel[:2] == ".." {
			t.Errorf("listed copy %q resolves outside %q", c.Path, e.root)
		}
	}
}

// TestOffsiteRestoreScratchPathMatchesTheListing pins the two halves together: the path the UI names
// in the completion flash must be the same path the listing (and therefore the delete button) uses.
// If these ever diverge, the customer is told about a directory the page cannot show or remove.
func TestOffsiteRestoreScratchPathMatchesTheListing(t *testing.T) {
	e := newVerifyCopyEnv(t, "immich")
	named := e.m.OffsiteRestoreScratchPath("immich")
	if named == "" {
		t.Fatal("OffsiteRestoreScratchPath returned empty — the flash would fall back to the vague wording")
	}
	var listed string
	for _, c := range e.m.ListOffsiteRestoreCopies() {
		if c.Stack == "immich" {
			listed = c.Path
		}
	}
	if named != listed {
		t.Errorf("flash names %q but the listing shows %q", named, listed)
	}
}
