package logger

import (
	"bufio"
	"encoding/json"
	"io"
	"os"
	"strings"
	"sync"
	"testing"
)

// serializeLoggerTests guards the global mutations (os.Stdout, globalLogger,
// zerolog global). These tests cannot run in parallel with each other.
var serializeLoggerTests sync.Mutex

// captureStdout swaps os.Stdout for a pipe, calls fn, restores stdout, and
// returns everything written during fn.
func captureStdout(t *testing.T, fn func()) string {
	t.Helper()

	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("os.Pipe: %v", err)
	}

	old := os.Stdout
	os.Stdout = w
	t.Cleanup(func() { os.Stdout = old })

	// Run fn and close writer so the reader unblocks.
	doneWrite := make(chan struct{})
	go func() {
		fn()
		_ = w.Close()
		close(doneWrite)
	}()

	var buf strings.Builder
	scanner := bufio.NewScanner(r)
	scanner.Buffer(make([]byte, 64*1024), 1024*1024)
	for scanner.Scan() {
		buf.WriteString(scanner.Text())
		buf.WriteByte('\n')
	}
	// Drain any residual (shouldn't happen after Close, but safe):
	_, _ = io.Copy(io.Discard, r)
	<-doneWrite
	return buf.String()
}

func TestInit_JSONFormatProducesParseableOutput(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "info", Format: "json"})
		Info().Str("user", "alice").Msg("hello")
	})

	// Find the first non-empty line; parse as JSON.
	var line string
	for l := range strings.SplitSeq(out, "\n") {
		if strings.TrimSpace(l) != "" {
			line = l
			break
		}
	}
	if line == "" {
		t.Fatalf("no log output captured; stdout = %q", out)
	}

	var parsed map[string]any
	if err := json.Unmarshal([]byte(line), &parsed); err != nil {
		t.Fatalf("log line is not valid JSON: %v\nline: %s", err, line)
	}

	// Field assertions — zerolog uses "message" for the msg and "level" for level.
	if got, _ := parsed["message"].(string); got != "hello" {
		t.Errorf("message = %v, want hello", parsed["message"])
	}
	if got, _ := parsed["user"].(string); got != "alice" {
		t.Errorf("user field = %v, want alice", parsed["user"])
	}
	if got, _ := parsed["level"].(string); got != "info" {
		t.Errorf("level = %v, want info", parsed["level"])
	}
	if _, ok := parsed["time"]; !ok {
		t.Errorf("expected time field; got keys %v", keysOf(parsed))
	}
	if _, ok := parsed["caller"]; !ok {
		t.Errorf("expected caller field; got keys %v", keysOf(parsed))
	}
}

func TestInit_LevelFilterDropsBelowThreshold(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "warn", Format: "json"})
		Debug().Msg("debug-dropped")
		Info().Msg("info-dropped")
		Warn().Msg("warn-kept")
		Error().Msg("error-kept")
	})

	if strings.Contains(out, "debug-dropped") {
		t.Errorf("debug event leaked through warn filter: %s", out)
	}
	if strings.Contains(out, "info-dropped") {
		t.Errorf("info event leaked through warn filter: %s", out)
	}
	if !strings.Contains(out, "warn-kept") {
		t.Errorf("warn event missing: %s", out)
	}
	if !strings.Contains(out, "error-kept") {
		t.Errorf("error event missing: %s", out)
	}
}

func TestInit_UnknownLevelDefaultsToInfo(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "gibberish", Format: "json"})
		Debug().Msg("debug-should-be-dropped")
		Info().Msg("info-should-appear")
	})

	if strings.Contains(out, "debug-should-be-dropped") {
		t.Errorf("debug leaked at default info level: %s", out)
	}
	if !strings.Contains(out, "info-should-appear") {
		t.Errorf("info missing at default info level: %s", out)
	}
}

func TestInit_TextFormatDoesNotCrashAndIsNotJSON(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "info", Format: "text"})
		Info().Str("k", "v").Msg("plain")
	})

	if !strings.Contains(out, "plain") {
		t.Errorf("text output missing message: %s", out)
	}
	// Console writer output is ANSI-colored key=value form, not JSON.
	var parsed map[string]any
	if json.Unmarshal([]byte(strings.Split(out, "\n")[0]), &parsed) == nil {
		t.Errorf("text format unexpectedly parsed as JSON: %s", out)
	}
}

func TestGet_AutoInitializesWhenUnused(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	// Forcibly clear the global so Get() hits the lazy-init branch.
	globalLogger = nil

	l := Get()
	if l == nil {
		t.Fatal("Get() returned nil; lazy init did not run")
	}
	if globalLogger == nil {
		t.Fatal("globalLogger still nil after Get()")
	}
}

func TestWithComponent_AddsComponentField(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "info", Format: "json"})
		comp := WithComponent("buckets")
		comp.Info().Msg("tagged")
	})

	line := firstNonEmptyLine(out)
	var parsed map[string]any
	if err := json.Unmarshal([]byte(line), &parsed); err != nil {
		t.Fatalf("not JSON: %v — %s", err, line)
	}
	if got, _ := parsed["component"].(string); got != "buckets" {
		t.Errorf("component = %v, want buckets", parsed["component"])
	}
}

func TestLogger_WithContext_AddsFields(t *testing.T) {
	serializeLoggerTests.Lock()
	defer serializeLoggerTests.Unlock()

	out := captureStdout(t, func() {
		Init(Config{Level: "info", Format: "json"})
		l := Get().WithContext(map[string]any{
			"request_id": "req-42",
			"attempt":    2,
		})
		l.Info().Msg("ctx")
	})

	line := firstNonEmptyLine(out)
	var parsed map[string]any
	if err := json.Unmarshal([]byte(line), &parsed); err != nil {
		t.Fatalf("not JSON: %v — %s", err, line)
	}
	if parsed["request_id"] != "req-42" {
		t.Errorf("request_id = %v", parsed["request_id"])
	}
	// JSON numbers decode to float64.
	if got, _ := parsed["attempt"].(float64); got != 2 {
		t.Errorf("attempt = %v, want 2", parsed["attempt"])
	}
}

// --- helpers ---

func firstNonEmptyLine(s string) string {
	for l := range strings.SplitSeq(s, "\n") {
		if strings.TrimSpace(l) != "" {
			return l
		}
	}
	return ""
}

func keysOf(m map[string]any) []string {
	out := make([]string, 0, len(m))
	for k := range m {
		out = append(out, k)
	}
	return out
}
