package backup

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

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

func newTestSettings(t *testing.T) *settings.Settings {
	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 sett
}

// R-100 — LastRun records an ATTEMPT; LastSuccess records a RESULT.
//
// The defect these pin: `LastRun` is written unconditionally at the end of every offsite run, failures
// included, so the hub's staleness verdict ("how long since LastRun?") was really asking "how long
// since we last TRIED?" — and a tier failing on every single run read as perfectly fresh forever.
//
// These are the CONTROLLER half (does the anchor move only on success, and does it survive the writes
// that rebuild the target?). The hub half — does the verdict count from it — lives in the hub's
// offsite tests.

// The invariant named by the comment at the write site, per the standing rule that an asserted
// invariant needs a test pinning it. This calls the PRODUCTION rule — an earlier version of this test
// re-implemented it in a local closure and was hollow: mutating offbox.go left it green.
//
// RED-PROOF: make offboxAnchorAfterRun return `at` unconditionally (drop the runErr guard) → this
// fails with "a FAILED run advanced LastSuccess — that is the R-100 defect in mirror image".
func TestOffboxAnchorAfterRun_FailureNeitherAdvancesNorClears(t *testing.T) {
	const monday = "2026-07-20T02:15:00Z"
	boom := errors.New("restic: connection refused")

	anchor := offboxAnchorAfterRun("", monday, nil)
	if anchor != monday {
		t.Fatalf("precondition: a successful run must set the anchor, got %q", anchor)
	}

	// Five consecutive failing nights. The attempt clock moves; the anchor must not.
	for _, night := range []string{
		"2026-07-21T02:15:00Z", "2026-07-22T02:15:00Z", "2026-07-23T02:15:00Z",
		"2026-07-24T02:15:00Z", "2026-07-25T02:15:00Z",
	} {
		anchor = offboxAnchorAfterRun(anchor, night, boom)
		if anchor == night {
			t.Fatalf("a FAILED run advanced LastSuccess to %q — that is the R-100 defect in mirror image", anchor)
		}
		if anchor != monday {
			t.Fatalf("a FAILED run CLEARED or moved the anchor (got %q, want %q) — one bad night must not make an established tier read as never-succeeded", anchor, monday)
		}
	}
}

// Recovery: a later success moves it forward, or a tier would stay permanently stale after one good
// night.
//
// RED-PROOF: make offboxAnchorAfterRun return `prev` unconditionally → this fails with
// "a successful run did not advance the anchor".
func TestOffboxAnchorAfterRun_SuccessAdvances(t *testing.T) {
	got := offboxAnchorAfterRun("2026-07-20T02:15:00Z", "2026-07-26T02:15:00Z", nil)
	if got != "2026-07-26T02:15:00Z" {
		t.Errorf("a successful run did not advance the anchor: %q", got)
	}
}

// A never-run tier stays empty on failure — it must not acquire a fabricated anchor, because "" is the
// signal the hub's newborn-box path keys on.
func TestOffboxAnchorAfterRun_NeverRanStaysEmptyOnFailure(t *testing.T) {
	if got := offboxAnchorAfterRun("", "2026-07-21T02:15:00Z", errors.New("boom")); got != "" {
		t.Errorf("a failed first run fabricated an anchor (%q) — the newborn-box path keys on empty", got)
	}
}

// The wire carries it. A field the hub cannot see is a field that does not exist — the "seam built but
// never wired" class this project has hit four times.
//
// RED-PROOF: drop `LastSuccess: t.LastSuccess` from OffboxReportStatus() → this fails with
// "OffboxReportStatus dropped LastSuccess — the hub would degrade forever on a controller that has it".
func TestOffboxReportStatus_CarriesLastSuccess(t *testing.T) {
	m := &Manager{settings: newTestSettings(t)}
	if err := m.settings.SetOffboxTarget(&settings.OffboxTarget{
		Enabled:     true,
		Host:        "nas.example",
		User:        "u1",
		RepoPath:    "/vol/repo",
		EscrowState: "escrowed",
		LastRun:     "2026-07-26T02:15:00Z",
		LastStatus:  "ok",
		LastSuccess: "2026-07-26T02:15:00Z",
	}); err != nil {
		t.Fatalf("seed: %v", err)
	}
	got := m.OffboxReportStatus()
	if got == nil {
		t.Fatal("OffboxReportStatus returned nil for an enabled target")
	}
	if got.LastSuccess != "2026-07-26T02:15:00Z" {
		t.Errorf("OffboxReportStatus dropped LastSuccess — the hub would degrade forever on a controller that has it (got %q)", got.LastSuccess)
	}
}

// A re-apply from the hub is not a new tier. Dropping the anchor here would reset an established tier
// to "never succeeded" every time the hub re-pushes its descriptor.
//
// RED-PROOF: remove `tgt.LastSuccess = cur.LastSuccess` from ApplyOffsiteTarget's carry-over block →
// this fails with "a hub re-apply erased the staleness anchor".
func TestApplyOffsiteTarget_PreservesLastSuccess(t *testing.T) {
	m := &Manager{settings: newTestSettings(t)}
	if err := m.settings.SetOffboxTarget(&settings.OffboxTarget{
		Enabled: true, Host: "nas.example", User: "u1", RepoPath: "/vol/repo",
		EscrowState: "escrowed", LastSuccess: "2026-07-26T02:15:00Z", LastRun: "2026-07-27T02:15:00Z",
	}); err != nil {
		t.Fatalf("seed: %v", err)
	}
	cur := m.settings.GetOffboxTarget()
	// Mirror ApplyOffsiteTarget's carry-over onto a freshly-built target.
	tgt := &settings.OffboxTarget{Enabled: true, Host: "nas.example", User: "u1", RepoPath: "/vol/repo", Schedule: "daily"}
	tgt.EscrowState = cur.EscrowState
	tgt.LastRun, tgt.LastStatus, tgt.LastError = cur.LastRun, cur.LastStatus, cur.LastError
	tgt.LastSuccess = cur.LastSuccess
	if tgt.LastSuccess != "2026-07-26T02:15:00Z" {
		t.Errorf("a hub re-apply erased the staleness anchor (got %q)", tgt.LastSuccess)
	}
}
