package backup

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

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

// atomicTestManager wires a Manager whose GetAppDrivePath resolves to a single drive dir, with the
// F7 tar seam injected. Returns the manager + the volume-dump dir where `.tar`/`.tar.tmp` land.
func atomicTestManager(t *testing.T, stack, volName string, tar func(volName, dumpDir string) ([]byte, error)) (*Manager, string) {
	t.Helper()
	drive := t.TempDir()
	cfg := &config.Config{}
	cfg.Paths.SystemDataPath = drive
	fake := &volDumpFakeProvider{
		stacks:  []StackSummary{{Name: stack}},
		volumes: map[string][]string{stack: {volName}},
	}
	m := &Manager{cfg: cfg, logger: log.New(io.Discard, "", 0),
		systemDataPath: drive, stackProvider: fake, tarVolume: tar}
	dumpDir := AppVolumeDumpPath(m.namespaceRoot(drive), stack)
	return m, dumpDir
}

// Happy path: the tar seam writes a good `.tar.tmp`; DumpAppVolumes promotes it to `.tar`, leaves no
// `.tmp`, and the content is exactly what the seam wrote.
func TestDumpAppVolumes_HappyAtomicPromote(t *testing.T) {
	const vol = "app_data"
	want := []byte("GOOD-TAR-CONTENT")
	m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
		return nil, os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), want, 0644)
	})

	if err := m.DumpAppVolumes("app"); err != nil {
		t.Fatalf("DumpAppVolumes: %v", err)
	}
	got, err := os.ReadFile(filepath.Join(dumpDir, vol+".tar"))
	if err != nil {
		t.Fatalf("final .tar missing: %v", err)
	}
	if string(got) != string(want) {
		t.Errorf("final .tar content = %q, want %q", got, want)
	}
	if _, err := os.Stat(filepath.Join(dumpDir, vol+".tar.tmp")); !os.IsNotExist(err) {
		t.Errorf("a `.tar.tmp` was left behind after a successful dump")
	}
}

// THE F7 RED-PROOF: a good `.tar` already exists (the last restore point). The tar seam fails
// mid-write (writes a partial/0-byte `.tar.tmp` then errors, simulating a NFS cut). The ORIGINAL
// `.tar` MUST be byte-identical afterwards, and no 0-byte `.tar` may exist. Companion: revert
// DumpAppVolumes to the in-place `tar cf …/<vol>.tar` write → the marker is truncated → this fails.
func TestDumpAppVolumes_MidWriteFailurePreservesLastGood(t *testing.T) {
	const vol = "app_data"
	marker := []byte("LAST-GOOD-247-BYTE-DUMP-MARKER") // the campaign's 247-byte last-good dump, in spirit

	m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
		// Simulate a mid-write cut: partially write the tmp, then fail (as a dead NFS target would).
		_ = os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), []byte("PARTIAL-TRUNCATED"), 0644)
		return []byte("tar: write error: Input/output error"), errTest
	})
	// Pre-seed the last good restore point.
	if err := os.MkdirAll(dumpDir, 0755); err != nil {
		t.Fatal(err)
	}
	tarPath := filepath.Join(dumpDir, vol+".tar")
	if err := os.WriteFile(tarPath, marker, 0644); err != nil {
		t.Fatal(err)
	}

	err := m.DumpAppVolumes("app")
	if err == nil {
		t.Fatal("DumpAppVolumes must report failure when the tar step fails")
	}

	// The restore point is byte-identical — F7's whole point.
	got, rerr := os.ReadFile(tarPath)
	if rerr != nil {
		t.Fatalf("the last good `.tar` was DESTROYED by a failed write (F7 regression): %v", rerr)
	}
	if string(got) != string(marker) {
		t.Errorf("last good `.tar` was mutated by a failed write: got %q, want %q (F7 regression)", got, marker)
	}
	// No stray tmp and no 0-byte tar.
	if _, err := os.Stat(filepath.Join(dumpDir, vol+".tar.tmp")); !os.IsNotExist(err) {
		t.Errorf("the failed `.tar.tmp` was not cleaned up")
	}
	if info, _ := os.Stat(tarPath); info != nil && info.Size() == 0 {
		t.Errorf("the restore point is now a 0-byte file (the exact F7 failure)")
	}
}

// A leftover `.tar.tmp` from a previously-killed run is cleaned up by the next dump and is never a
// restore point (its name ends `.tmp`, invisible to the `.tar` restore-point scan).
func TestDumpAppVolumes_LeftoverTmpCleaned(t *testing.T) {
	const vol = "app_data"
	m, dumpDir := atomicTestManager(t, "app", vol, func(_, dir string) ([]byte, error) {
		return nil, os.WriteFile(filepath.Join(dir, vol+".tar.tmp"), []byte("new"), 0644)
	})
	if err := os.MkdirAll(dumpDir, 0755); err != nil {
		t.Fatal(err)
	}
	// A stale tmp for a DIFFERENT (removed) volume — must be swept.
	stale := filepath.Join(dumpDir, "old_removed_vol.tar.tmp")
	if err := os.WriteFile(stale, []byte("stale"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := m.DumpAppVolumes("app"); err != nil {
		t.Fatalf("DumpAppVolumes: %v", err)
	}
	if _, err := os.Stat(stale); !os.IsNotExist(err) {
		t.Errorf("a stale `.tar.tmp` was left behind (must be swept)")
	}
}
