package backup

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

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

// R-101 Part 2 — THE COPY-SITE HAZARD, exercised through the REAL record* helpers.
//
// The three record* helpers used to build a WHOLE CrossDriveBackup literal, with a helper re-applying
// exactly two fields. Anything not named in the literal was zeroed on every status write. Adding
// LastSuccess to that shape would have had `recordTier2Failure` CLEAR the anchor — the mirror image of
// the defect being fixed, and firing on the FIRST failure rather than lying dormant.
//
// The R-100 lesson applies: these call the production functions. A test that modelled the copy in a
// closure would stay green through any mutation of the real code.

func anchorMgr(t *testing.T) *Manager {
	t.Helper()
	sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
	if err != nil {
		t.Fatalf("settings.Load: %v", err)
	}
	return &Manager{settings: sett, logger: log.New(io.Discard, "", 0)}
}

func tgt() *Tier2Target { return &Tier2Target{NamespaceRoot: "/mnt/hdd_1/felhom-data/calibre-web"} }

// SCENARIO F — a FAILURE must not wipe the anchor. This is the Part 2 proof and it runs the real
// recordTier2Success → recordTier2Failure sequence.
//
// RED-PROOF: make recordTier2Failure build a whole literal again (or add `c.LastSuccess = ""` to its
// mutation) → this fails with "a FAILED run wiped the success anchor".
func TestTier2_FailureDoesNotWipeTheAnchor(t *testing.T) {
	m := anchorMgr(t)

	// Seed a row with a DISTINCT, older anchor (a real success on the 25th) already tracked, so the
	// anchor and the attempt clock are distinguishable — a success recorded in this same test would
	// land in the same second as the failures and prove nothing about which field moved.
	const anchor = "2026-07-25T01:30:00Z"
	if err := m.settings.SetCrossDriveConfig("calibre-web", &settings.CrossDriveBackup{
		Enabled: true, Method: "rsync", Schedule: "daily",
		LastRun: anchor, LastStatus: "ok", LastSuccess: anchor, SuccessTracked: true,
	}); err != nil {
		t.Fatal(err)
	}

	// Five consecutive failing nights, through the real helper.
	for i := 0; i < 5; i++ {
		m.recordTier2Failure("calibre-web", tgt(), errors.New("rsync: connection reset"))
		got := m.settings.GetCrossDriveConfig("calibre-web")
		if got.LastSuccess == "" {
			t.Fatalf("a FAILED run wiped the success anchor (round %d) — one bad night would read as 'no copy has ever succeeded'", i+1)
		}
		if got.LastSuccess != anchor {
			t.Fatalf("a FAILED run MOVED the anchor to %q (was %q) — that is the R-101 defect in mirror image", got.LastSuccess, anchor)
		}
		if got.LastStatus != "error" {
			t.Errorf("the failure was not recorded (status=%q)", got.LastStatus)
		}
		// the ATTEMPT clock DOES move — that is what made the old rendering wrong
		if got.LastRun == anchor {
			t.Errorf("LastRun did not advance on the attempt — it still reads the old success time %q", got.LastRun)
		}
	}
}

// A successful run advances the anchor, or a tier would look permanently stale after one good night.
func TestTier2_SuccessAdvancesTheAnchor(t *testing.T) {
	m := anchorMgr(t)
	if err := m.settings.SetCrossDriveConfig("calibre-web", &settings.CrossDriveBackup{
		Enabled: true, Method: "rsync", Schedule: "daily",
		LastRun: "2026-07-25T01:30:00Z", LastStatus: "error",
		LastSuccess: "2026-07-25T01:30:00Z", SuccessTracked: true,
	}); err != nil {
		t.Fatal(err)
	}
	m.recordTier2Success("calibre-web", tgt(), 1<<30, "", 0)
	got := m.settings.GetCrossDriveConfig("calibre-web")
	if got.LastSuccess == "2026-07-25T01:30:00Z" {
		t.Error("a successful run did not advance the anchor")
	}
	if got.LastSuccess != got.LastRun {
		t.Errorf("the anchor and the attempt clock disagree after a success (%q vs %q)", got.LastSuccess, got.LastRun)
	}
	if got.LastError != "" {
		t.Errorf("a success left the previous error in place (%q)", got.LastError)
	}
}

// A customer preference must still survive a status write — the behaviour the old helper existed to
// provide. The new copy-the-row form should give this for free, and this pins that it does.
//
// RED-PROOF: make tier2Update start from a zero-value struct instead of copying the existing row →
// this fails with "a status write wiped the customer's Tier-2 preference".
func TestTier2_StatusWritePreservesCustomerPreference(t *testing.T) {
	m := anchorMgr(t)
	if err := m.settings.SetTier2Preference("calibre-web", true, "/mnt/hdd_2"); err != nil {
		t.Fatal(err)
	}
	m.recordTier2Failure("calibre-web", tgt(), errors.New("boom"))

	got := m.settings.GetCrossDriveConfig("calibre-web")
	if !got.UserDisabled || got.PreferredTarget != "/mnt/hdd_2" {
		t.Errorf("a status write wiped the customer's Tier-2 preference (UserDisabled=%v PreferredTarget=%q)",
			got.UserDisabled, got.PreferredTarget)
	}
}

// A no-target write must not destroy the anchor either: "there is no destination drive right now" is
// not evidence that the last successful copy never happened.
func TestTier2_NoTargetKeepsTheAnchor(t *testing.T) {
	m := anchorMgr(t)
	m.recordTier2Success("calibre-web", tgt(), 1<<30, "", 0)
	anchor := m.settings.GetCrossDriveConfig("calibre-web").LastSuccess

	m.recordTier2NoTarget("calibre-web", "nincs elérhető második meghajtó")
	got := m.settings.GetCrossDriveConfig("calibre-web")
	if got.LastSuccess != anchor {
		t.Errorf("a no_target write lost the anchor (%q, was %q)", got.LastSuccess, anchor)
	}
	if got.LastRun != "" {
		t.Errorf("no_target should clear the attempt clock as before, got %q", got.LastRun)
	}
}

// SCENARIO E (producer half) — a LEGACY row is migrated truthfully on first touch: a row whose last
// known state was a SUCCESS adopts that time as its anchor, so the deploy does not flip every existing
// customer to "never succeeded".
//
// RED-PROOF: delete the `if !cfg.SuccessTracked` seeding block in tier2Update → this fails with
// "a legacy OK row was not migrated".
func TestTier2_LegacyOkRowSeedsItsAnchor(t *testing.T) {
	m := anchorMgr(t)
	// A pre-R-101 row: status ok, a last_run, no anchor, not tracked.
	if err := m.settings.SetCrossDriveConfig("calibre-web", &settings.CrossDriveBackup{
		Enabled: true, Method: "rsync", Schedule: "daily",
		LastRun: "2026-07-28T01:30:00Z", LastStatus: "ok",
	}); err != nil {
		t.Fatal(err)
	}
	// The next run fails — the first new-code touch of this row.
	m.recordTier2Failure("calibre-web", tgt(), errors.New("boom"))

	got := m.settings.GetCrossDriveConfig("calibre-web")
	if !got.SuccessTracked {
		t.Fatal("the row was not marked as tracked")
	}
	if got.LastSuccess != "2026-07-28T01:30:00Z" {
		t.Errorf("a legacy OK row was not migrated — its known-good run should have become the anchor (got %q)", got.LastSuccess)
	}
}

// ...but a legacy row whose last known state was an ERROR seeds NOTHING: the old data contains no
// evidence of a success, and inventing one would be the original defect.
func TestTier2_LegacyErrorRowSeedsNothing(t *testing.T) {
	m := anchorMgr(t)
	if err := m.settings.SetCrossDriveConfig("calibre-web", &settings.CrossDriveBackup{
		Enabled: true, Method: "rsync", Schedule: "daily",
		LastRun: "2026-07-28T01:30:00Z", LastStatus: "error",
	}); err != nil {
		t.Fatal(err)
	}
	m.recordTier2Failure("calibre-web", tgt(), errors.New("boom"))

	if got := m.settings.GetCrossDriveConfig("calibre-web"); got.LastSuccess != "" {
		t.Errorf("a legacy ERROR row invented an anchor (%q) — that is the defect, not the fix", got.LastSuccess)
	}
}
