package hub

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

// recordingReporter records each pushed report and serves a scripted per-call
// (envelope, error) sequence — the S2 heartbeat log-pull harness.
type recordingReporter struct {
	reports []*HostReport
	script  []struct {
		env *ControlEnvelope
		err error
	}
}

func (r *recordingReporter) Report(_ context.Context, rep *HostReport) (*ControlEnvelope, error) {
	// Copy the LogTail pointer state at push time (the loop reuses collector reports).
	cp := *rep
	r.reports = append(r.reports, &cp)
	i := len(r.reports) - 1
	if i < len(r.script) {
		return r.script[i].env, r.script[i].err
	}
	return &ControlEnvelope{}, nil
}

func tailLoop(rep *recordingReporter) *Loop {
	var cn int32
	l := NewLoop(&fakeCollector{report: &HostReport{}, n: &cn}, rep, time.Hour, quietLogger())
	l.SetLogTailSource(func(maxBytes int) []string { return []string{"line-a", "line-b"} })
	return l
}

// S2 (agent half): an envelope's log_tail_requested arms the pull; the NEXT report
// carries log_tail; the one after (request cleared hub-side) carries nothing —
// consume-once. Companion red-proof: drop the `l.logTailPending = false` drain →
// report 3 also carries a tail → the last assertion fails.
func TestLoop_LogTailRequestedShipsOnNextReportOnce(t *testing.T) {
	rep := &recordingReporter{script: []struct {
		env *ControlEnvelope
		err error
	}{
		{env: &ControlEnvelope{LogTailRequested: true}},
		{env: &ControlEnvelope{}}, // the tail arrived — hub cleared the request
		{env: &ControlEnvelope{}},
	}}
	l := tailLoop(rep)
	ctx := context.Background()
	l.cycle(ctx, time.Hour)
	l.cycle(ctx, time.Hour)
	l.cycle(ctx, time.Hour)

	if len(rep.reports) != 3 {
		t.Fatalf("reports = %d, want 3", len(rep.reports))
	}
	if rep.reports[0].LogTail != nil {
		t.Errorf("report 1 must not carry a tail (the request only arrived in its envelope)")
	}
	got := rep.reports[1].LogTail
	if got == nil || len(got.Lines) != 2 || got.Lines[0] != "line-a" || got.CollectedAt == "" {
		t.Fatalf("report 2 log_tail = %+v, want the 2 ring lines + collected_at", got)
	}
	if rep.reports[2].LogTail != nil {
		t.Errorf("report 3 carries a tail again — consume-once broken: %+v", rep.reports[2].LogTail)
	}
}

// S2 companion (fail-safe retry): the push CARRYING the tail fails → the local pending
// is spent, but the hub's request is still pending, so the next envelope re-arms it and
// the following report fulfills. Asserts the retry ships the tail exactly once more.
func TestLoop_FailedTailPushIsReArmedByNextEnvelope(t *testing.T) {
	rep := &recordingReporter{script: []struct {
		env *ControlEnvelope
		err error
	}{
		{env: &ControlEnvelope{LogTailRequested: true}},          // arm
		{err: errors.New("hub 5xx")},                             // the carrying push FAILS
		{env: &ControlEnvelope{LogTailRequested: true}},          // hub still pending → re-arm
		{env: &ControlEnvelope{}},                                // fulfilled
	}}
	l := tailLoop(rep)
	ctx := context.Background()
	for i := 0; i < 4; i++ {
		l.cycle(ctx, time.Hour)
	}
	if len(rep.reports) != 4 {
		t.Fatalf("reports = %d, want 4", len(rep.reports))
	}
	if rep.reports[1].LogTail == nil {
		t.Errorf("report 2 (the failed push) should have carried the tail")
	}
	if rep.reports[2].LogTail != nil {
		t.Errorf("report 3 must not carry a tail (pending was spent; envelope re-arms only after it)")
	}
	if rep.reports[3].LogTail == nil {
		t.Errorf("report 4 must fulfill the re-armed request — retry lost")
	}
}

// No source wired → the request is ignored (clean no-op, no panic).
func TestLoop_LogTailRequestIgnoredWithoutSource(t *testing.T) {
	rep := &recordingReporter{script: []struct {
		env *ControlEnvelope
		err error
	}{
		{env: &ControlEnvelope{LogTailRequested: true}},
		{env: &ControlEnvelope{}},
	}}
	var cn int32
	l := NewLoop(&fakeCollector{report: &HostReport{}, n: &cn}, rep, time.Hour, quietLogger())
	ctx := context.Background()
	l.cycle(ctx, time.Hour)
	l.cycle(ctx, time.Hour)
	if rep.reports[1].LogTail != nil {
		t.Errorf("tail shipped with no source wired: %+v", rep.reports[1].LogTail)
	}
}
