package report

import (
	"bytes"
	"encoding/json"
	"log"
	"strings"
	"testing"
	"time"
)

func resetSelfTail() {
	SetPendingControllerLog(false)
	SetControllerLogSource(nil)
}

// S2 (controller half): the ACK's controller_log_requested arms the pull; the next
// build attaches the ring tail; the build after that attaches nothing (consume-once).
// Companion red-proof: remove the `selfTailPending = false` drain → the second build
// ships again → the nil assertion fails.
func TestControllerLogTail_ConsumeOnce(t *testing.T) {
	t.Cleanup(resetSelfTail)
	SetControllerLogSource(func(maxBytes int) []string { return []string{"l1", "l2", "l3"} })
	SetPendingControllerLog(true)

	var logbuf bytes.Buffer
	logger := log.New(&logbuf, "", 0)
	first := buildControllerLogTail(logger)
	if first == nil || len(first.Lines) != 3 || first.CollectedAt.IsZero() {
		t.Fatalf("first build tail = %+v, want the 3 ring lines + collected_at", first)
	}
	// Customer transparency: the pull is announced in the box's own log.
	if !strings.Contains(logbuf.String(), "operator log pull served") {
		t.Errorf("transparency INFO line missing: %q", logbuf.String())
	}
	if second := buildControllerLogTail(logger); second != nil {
		t.Errorf("second build shipped again — consume-once broken: %+v", second)
	}
}

// The hub is the source of truth: an ACK without the flag clears a stale pending.
func TestControllerLogTail_AckClears(t *testing.T) {
	t.Cleanup(resetSelfTail)
	SetControllerLogSource(func(int) []string { return []string{"x"} })
	SetPendingControllerLog(true)
	SetPendingControllerLog(false)
	if tail := buildControllerLogTail(nil); tail != nil {
		t.Errorf("cleared pending still shipped: %+v", tail)
	}
}

// No source wired (defensive) → pending is drained, nothing shipped, no panic.
func TestControllerLogTail_NoSource(t *testing.T) {
	t.Cleanup(resetSelfTail)
	SetPendingControllerLog(true)
	if tail := buildControllerLogTail(nil); tail != nil {
		t.Errorf("tail shipped with no source: %+v", tail)
	}
}

// S3 app-tail byte-compatibility: the v0.111.0 log_tails wire shape is unchanged by
// the additive controller_log_tail — absent in the steady state (omitempty), and the
// existing keys marshal identically. Companion red-proof: drop `omitempty` from
// ControllerLogTail → the steady-state JSON gains a null key → FAIL.
func TestReportSchema_AppTailUnchangedAndSelfTailAdditive(t *testing.T) {
	r := &Report{
		Version:    1,
		CustomerID: "c1",
		LogTails: []LogTail{{
			App: "gokapi", CollectedAt: time.Date(2026, 7, 11, 10, 0, 0, 0, time.UTC),
			Lines: []string{"a", "b"},
		}},
	}
	raw, err := json.Marshal(r)
	if err != nil {
		t.Fatal(err)
	}
	var m map[string]json.RawMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		t.Fatal(err)
	}
	if _, present := m["controller_log_tail"]; present {
		t.Errorf("controller_log_tail present in the steady state — must be omitted (additive)")
	}
	var tails []map[string]json.RawMessage
	if err := json.Unmarshal(m["log_tails"], &tails); err != nil || len(tails) != 1 {
		t.Fatalf("log_tails shape changed: %s", m["log_tails"])
	}
	for _, key := range []string{"app", "collected_at", "lines"} {
		if _, ok := tails[0][key]; !ok {
			t.Errorf("log_tails entry lost key %q — app-tail flow must stay byte-compatible", key)
		}
	}

	// The ACK parse: old fields + the new flag coexist; absent flag = false.
	var pr PushResponse
	if err := json.Unmarshal([]byte(`{"status":"ok","log_tail_requests":["gokapi"],"controller_log_requested":true}`), &pr); err != nil {
		t.Fatal(err)
	}
	if len(pr.LogTailRequests) != 1 || !pr.ControllerLogRequested {
		t.Errorf("ACK parse = %+v", pr)
	}
	var old PushResponse
	if err := json.Unmarshal([]byte(`{"status":"ok"}`), &old); err != nil || old.ControllerLogRequested {
		t.Errorf("old ACK must parse with the flag false: %+v err=%v", old, err)
	}
}
