package pbs

import (
	"context"
	"errors"
	"io"
	"log/slog"
	"testing"

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

type recordingSink struct {
	calls []struct {
		storage      string
		unauthorized bool
		detail       string
	}
}

func (s *recordingSink) NoteAuthResult(storageID string, unauthorized bool, detail string) {
	s.calls = append(s.calls, struct {
		storage      string
		unauthorized bool
		detail       string
	}{storageID, unauthorized, detail})
}

func newProbeReporter(t *testing.T, sink AuthSink, probeErr error) *LiveSnapshotReporter {
	t.Helper()
	targets := func(ctx context.Context) ([]Target, error) {
		return []Target{{Datastore: "felhom-offsite", StorageID: "felhom-pbs"}}, nil
	}
	r := NewLiveSnapshotReporter(targets, NewSnapshotStore(), 0, slog.New(slog.NewTextHandler(io.Discard, nil)))
	r.listSnapshots = func(context.Context, Target) ([]hub.PBSSnapshot, error) { return nil, nil }
	r.probeAuth = func(context.Context, Target) error { return probeErr }
	if sink != nil {
		r.SetAuthSink(sink)
	}
	return r
}

// R-39 leg (c): a 401 must reach the sink as UNAUTHORIZED — the signal the DR bridge turns into a
// loud auth_failed state.
func TestLiveReporter_ForwardsUnauthorized(t *testing.T) {
	sink := &recordingSink{}
	r := newProbeReporter(t, sink, ErrUnauthorized)
	r.PBSSnapshots(context.Background())

	if len(sink.calls) != 1 {
		t.Fatalf("sink calls = %d, want 1 (the probe must run on every collect)", len(sink.calls))
	}
	c := sink.calls[0]
	if !c.unauthorized || c.storage != "felhom-pbs" {
		t.Errorf("got %+v, want unauthorized for felhom-pbs", c)
	}
}

// A TRANSPORT error is UNKNOWN, never a rejection: reporting it as unauthorized would re-key a
// perfectly good credential on every network blip.
func TestLiveReporter_TransportErrorIsNotUnauthorized(t *testing.T) {
	sink := &recordingSink{}
	r := newProbeReporter(t, sink, errors.New("dial tcp 10.77.0.1:8007: connect: connection refused"))
	r.PBSSnapshots(context.Background())

	if len(sink.calls) != 1 {
		t.Fatalf("sink calls = %d, want 1", len(sink.calls))
	}
	if sink.calls[0].unauthorized {
		t.Error("an unreachable PBS was reported as unauthorized — every blip would burn a credential")
	}
	if sink.calls[0].detail == "" {
		t.Error("an inconclusive probe must carry a detail so the state is explainable")
	}
}

// A healthy credential clears: unauthorized=false with an EMPTY detail is the recovery signal the
// bridge keys on.
func TestLiveReporter_HealthyProbeIsACleanClear(t *testing.T) {
	sink := &recordingSink{}
	r := newProbeReporter(t, sink, nil)
	r.PBSSnapshots(context.Background())

	if len(sink.calls) != 1 || sink.calls[0].unauthorized || sink.calls[0].detail != "" {
		t.Fatalf("got %+v, want a clean clear (unauthorized=false, detail=\"\")", sink.calls)
	}
}

// THE WIRING GUARD. With no sink attached the reporter must not probe at all — and this test exists
// because that is exactly how the leg shipped inert the first time: the seam was built and main.go
// never called SetAuthSink, so probeAuth was never invoked and nothing failed.
//
// A unit test cannot assert main.go's wiring; the live STOP-1/STOP-2 evidence does that. What it CAN
// pin is the contract this depends on — no sink means no probe — so the inert case stays a
// deliberate, documented behaviour rather than an accident nobody notices twice.
func TestLiveReporter_NoSinkMeansNoProbe(t *testing.T) {
	probed := false
	targets := func(ctx context.Context) ([]Target, error) {
		return []Target{{Datastore: "d", StorageID: "s"}}, nil
	}
	r := NewLiveSnapshotReporter(targets, NewSnapshotStore(), 0, slog.New(slog.NewTextHandler(io.Discard, nil)))
	r.listSnapshots = func(context.Context, Target) ([]hub.PBSSnapshot, error) { return nil, nil }
	r.probeAuth = func(context.Context, Target) error { probed = true; return nil }
	// deliberately no SetAuthSink
	r.PBSSnapshots(context.Background())
	if probed {
		t.Error("probed with no sink attached — a request nothing consumes")
	}
}
