package web

import (
	"bytes"
	"io"
	"log"
	"strings"
	"testing"
	"time"
)

// S1 capture-at-info (controller half): with the v0.116.0 writer layout —
// MultiWriter(LevelFilterWriter(stdout, "info"), LogBuffer) — a [DEBUG] line
// reaches the RING and is ABSENT from stdout, while [INFO] reaches both.
// Companion red-proof: gate the ring behind the same filter (the pre-fix shape,
// where the ring only existed at logging.level=debug) → the ring assertion fails.
func TestCaptureAtInfo_RingHoldsDebugStdoutDoesNot(t *testing.T) {
	var stdout bytes.Buffer
	lb := NewLogBuffer(50)
	logger := log.New(io.MultiWriter(NewLevelFilterWriter(&stdout, "info"), lb), "", log.LstdFlags)

	logger.Printf("[DEBUG] [web] netstorage add \"vids\" phase agent_add -> verifying")
	logger.Printf("[INFO] [web] network storage added + verified: vids")

	entries, total := lb.Entries("DEBUG", 0, time.Time{})
	if total != 2 || len(entries) != 2 {
		t.Fatalf("ring holds %d entries (returned %d), want 2 — DEBUG must be captured at logging.level=info", total, len(entries))
	}
	if entries[0].Level != "DEBUG" || !strings.Contains(entries[0].Message, "phase agent_add") {
		t.Errorf("ring entry 0 = %+v, want the DEBUG phase line", entries[0])
	}
	if strings.Contains(stdout.String(), "phase agent_add") {
		t.Errorf("stdout carries the DEBUG line at level info:\n%s", stdout.String())
	}
	if !strings.Contains(stdout.String(), "added + verified") {
		t.Errorf("stdout missing the INFO line:\n%s", stdout.String())
	}
}

// The filter respects higher minimums too (warn drops INFO) and passes untagged lines at info.
func TestLevelFilterWriter_Thresholds(t *testing.T) {
	cases := []struct {
		min   string
		line  string
		wants bool
	}{
		{"info", "[DEBUG] x", false},
		{"info", "[INFO] x", true},
		{"info", "untagged line", true}, // parses as INFO
		{"warn", "[INFO] x", false},
		{"warn", "[ERROR] x", true},
		{"debug", "[DEBUG] x", true},
	}
	for _, c := range cases {
		var out bytes.Buffer
		logger := log.New(NewLevelFilterWriter(&out, c.min), "", log.LstdFlags)
		logger.Printf("%s", c.line)
		got := strings.Contains(out.String(), c.line)
		if got != c.wants {
			t.Errorf("min=%s line=%q passed=%v want %v", c.min, c.line, got, c.wants)
		}
	}
}

// Lines renders chronological plain-text lines and honors the byte budget by
// keeping the NEWEST lines (the controller_log_tail 128 KB cap).
func TestLogBufferLines_ByteBudgetKeepsNewest(t *testing.T) {
	lb := NewLogBuffer(10)
	logger := log.New(lb, "", log.LstdFlags)
	for i := 0; i < 5; i++ {
		logger.Printf("[INFO] line-%d %s", i, strings.Repeat("x", 80))
	}
	all := lb.Lines(0)
	if len(all) != 5 || !strings.Contains(all[4], "line-4") {
		t.Fatalf("uncapped lines wrong: %d %v", len(all), all)
	}
	budget := len(all[3]) + len(all[4]) + 2
	capped := lb.Lines(budget)
	if len(capped) >= 5 || !strings.Contains(capped[len(capped)-1], "line-4") {
		t.Errorf("byte cap kept %d lines, newest=%q — must drop oldest first", len(capped), capped[len(capped)-1])
	}
}
