package pbs

import (
	"context"
	"errors"
	"testing"
	"time"

	"gitea.dooplex.hu/admin/felhom-agent/internal/hub"
)

// fakeReporter builds a LiveSnapshotReporter with an injected snapshot-lister seam and a targets
// closure, so no live PBS is needed. listFn is keyed by datastore; targetsErr forces a targets-
// resolution failure. The Target.Client is a throwaway &Client{} the seam never dereferences.
func fakeReporter(store *SnapshotStore, datastores []string, targetsErr error,
	listFn func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error)) *LiveSnapshotReporter {
	targets := func(ctx context.Context) ([]Target, error) {
		if targetsErr != nil {
			return nil, targetsErr
		}
		out := make([]Target, 0, len(datastores))
		for _, ds := range datastores {
			out = append(out, Target{Datastore: ds, Client: &Client{}})
		}
		return out, nil
	}
	r := NewLiveSnapshotReporter(targets, store, 2*time.Second, nil)
	r.listSnapshots = func(ctx context.Context, t Target) ([]hub.PBSSnapshot, error) {
		return listFn(ctx, t.Datastore)
	}
	return r
}

func snap(ds, backupID, backupTime string) hub.PBSSnapshot {
	return hub.PBSSnapshot{Namespace: "root", BackupType: "ct", BackupID: backupID, BackupTime: backupTime}
}

// T1 (load-bearing): coord present with NO prior verify. A fresh store (verify never ran) + a live
// lister returning 2 snapshots → PBSSnapshots returns both, and feeding them through
// BuildDRRecipeHostHalf yields a non-nil pbs whose latest_snapshot_id is the LATER snapshot.
func TestLiveReporter_CoordPresentWithoutPriorVerify(t *testing.T) {
	store := NewSnapshotStore() // empty — simulates a just-restarted daemon, no verify yet
	r := fakeReporter(store, []string{"felhom-spike"}, nil,
		func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error) {
			return []hub.PBSSnapshot{
				snap(ds, "9201", "2026-06-13T20:00:00Z"),
				snap(ds, "9201", "2026-06-16T17:00:00Z"), // latest
			}, nil
		})

	got := r.PBSSnapshots(context.Background())
	if len(got) != 2 {
		t.Fatalf("want 2 live snapshots, got %d", len(got))
	}

	// The whole point: a recipe built from the live read carries the pbs coord.
	h := hub.BuildDRRecipeHostHalf(nil,
		[]hub.StorageTarget{{Name: "felhom-pbs", Type: hub.StorageTypePBS, Content: "backup"}}, got)
	if h.PBS == nil {
		t.Fatal("pbs coord absent despite a reachable PBS — the gap this fixes")
	}
	if h.PBS.RepoID != "felhom-pbs" || h.PBS.Namespace != "root" || h.PBS.LatestSnapshotID != "9201" {
		t.Errorf("pbs coord = %+v, want felhom-pbs/root/9201", h.PBS)
	}

	// COMPANION (pre-fix): the bare SnapshotStore (no live read) with an empty store omits pbs.
	bare := NewSnapshotStore()
	h2 := hub.BuildDRRecipeHostHalf(nil,
		[]hub.StorageTarget{{Name: "felhom-pbs", Type: hub.StorageTypePBS, Content: "backup"}},
		bare.PBSSnapshots(context.Background()))
	if h2.PBS != nil {
		t.Fatal("companion sanity: the bare store should yield NO pbs coord (proves the live read is load-bearing)")
	}
}

// T2: live error → last-known-good fallback (and the store is NOT clobbered).
func TestLiveReporter_ErrorFallsBackToLastKnownGood(t *testing.T) {
	store := NewSnapshotStore()
	store.Record("D", []hub.PBSSnapshot{snap("D", "9001", "2026-06-10T00:00:00Z")})

	r := fakeReporter(store, []string{"D"}, nil,
		func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error) {
			return nil, errors.New("pbs unreachable")
		})

	got := r.PBSSnapshots(context.Background())
	if len(got) != 1 || got[0].BackupID != "9001" {
		t.Fatalf("want the last-known-good snapshot on live error, got %+v", got)
	}
	// The store must still hold the prior set (a failed live read must not wipe it).
	if lkg := store.Get("D"); len(lkg) != 1 || lkg[0].BackupID != "9001" {
		t.Errorf("live error clobbered the store: %+v", lkg)
	}

	// COMPANION (pre-fix): without the fallback branch (return empty on error) the result is empty.
	noFallback := func(ctx context.Context) []hub.PBSSnapshot {
		targets, _ := r.targets(ctx)
		out := []hub.PBSSnapshot{}
		for _, tg := range targets {
			if _, err := r.listSnapshots(ctx, tg); err != nil {
				continue // the mutation: drop the LKG-append branch
			}
		}
		return out
	}
	if len(noFallback(context.Background())) != 0 {
		t.Fatal("companion sanity: the no-fallback variant should return empty")
	}
}

// T3: a successful response updates the store (last-known-good warm for a later error).
func TestLiveReporter_SuccessUpdatesStore(t *testing.T) {
	store := NewSnapshotStore() // empty
	r := fakeReporter(store, []string{"D"}, nil,
		func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error) {
			return []hub.PBSSnapshot{snap("D", "9201", "2026-06-16T00:00:00Z")}, nil
		})
	_ = r.PBSSnapshots(context.Background())
	if lkg := store.Get("D"); len(lkg) != 1 || lkg[0].BackupID != "9201" {
		t.Errorf("store not warmed by a successful live read: %+v", lkg)
	}
}

// T4: targets-resolution error → the full last-known-good aggregate (not empty).
func TestLiveReporter_TargetsErrorReturnsAggregate(t *testing.T) {
	store := NewSnapshotStore()
	store.Record("D1", []hub.PBSSnapshot{snap("D1", "1", "2026-06-10T00:00:00Z")})
	store.Record("D2", []hub.PBSSnapshot{snap("D2", "2", "2026-06-11T00:00:00Z")})

	r := fakeReporter(store, nil, errors.New("ListStorage failed"),
		func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error) {
			t.Fatal("listSnapshots must not be called when targets fail to resolve")
			return nil, nil
		})
	got := r.PBSSnapshots(context.Background())
	if len(got) != 2 {
		t.Fatalf("want the 2-snapshot aggregate on a targets-resolution error, got %d (%+v)", len(got), got)
	}
}

// T5: bounded. A lister that blocks until ctx is done must return within ~timeout (the child-ctx
// deadline) and fall back, not hang. A short timeout keeps the test fast + deterministic.
func TestLiveReporter_BoundedByTimeout(t *testing.T) {
	store := NewSnapshotStore()
	store.Record("D", []hub.PBSSnapshot{snap("D", "9001", "2026-06-10T00:00:00Z")})

	targets := func(ctx context.Context) ([]Target, error) {
		return []Target{{Datastore: "D", Client: &Client{}}}, nil
	}
	r := NewLiveSnapshotReporter(targets, store, 100*time.Millisecond, nil)
	r.listSnapshots = func(ctx context.Context, t Target) ([]hub.PBSSnapshot, error) {
		<-ctx.Done() // block until the child-ctx deadline fires
		return nil, ctx.Err()
	}

	done := make(chan []hub.PBSSnapshot, 1)
	go func() { done <- r.PBSSnapshots(context.Background()) }()
	select {
	case got := <-done:
		// Fell back to last-known-good rather than hanging.
		if len(got) != 1 || got[0].BackupID != "9001" {
			t.Errorf("want last-known-good after the deadline, got %+v", got)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("PBSSnapshots hung past the timeout — not bounded by the child ctx")
	}
}

// T6: an empty-but-successful response is authoritative — it OVERWRITES a prior non-empty set.
// (Documents the chosen semantics: a real empty datastore must be reflected, not masked by stale LKG.)
func TestLiveReporter_EmptySuccessIsAuthoritative(t *testing.T) {
	store := NewSnapshotStore()
	store.Record("D", []hub.PBSSnapshot{snap("D", "9001", "2026-06-10T00:00:00Z")})

	r := fakeReporter(store, []string{"D"}, nil,
		func(ctx context.Context, ds string) ([]hub.PBSSnapshot, error) {
			return []hub.PBSSnapshot{}, nil // success, but the datastore is now empty
		})
	got := r.PBSSnapshots(context.Background())
	if len(got) != 0 {
		t.Errorf("empty-but-successful should yield an empty result, got %+v", got)
	}
	if lkg := store.Get("D"); len(lkg) != 0 {
		t.Errorf("empty success should overwrite the store to empty, got %+v", lkg)
	}
}
