package logger

import (
	"context"
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"testing"

	"github.com/go-logr/logr"
	"github.com/go-logr/zapr"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"go.uber.org/zap/zaptest/observer"

	"github.com/tidbcloud/aisupport/packages/sdk-go/infra/config"
)

func newObservedLogger() (logr.Logger, *observer.ObservedLogs) {
	core, logs := observer.New(zapcore.DebugLevel)
	return zapr.NewLogger(zap.New(core)), logs
}

func newTraceContext() context.Context {
	spanCtx := trace.NewSpanContext(trace.SpanContextConfig{
		TraceID:    trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
		TraceFlags: trace.FlagsSampled,
	})
	return trace.ContextWithSpanContext(context.Background(), spanCtx)
}

func TestFromContextOrDefUsesDefaultLoggerAndTraceID(t *testing.T) {
	t.Parallel()

	def, logs := newObservedLogger()
	logger := FromContextOrDef(newTraceContext(), def, "request_id", "req-1")
	logger.Info("default logger")

	entries := logs.AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("expected 1 log entry, got %d", len(entries))
	}

	ctx := entries[0].ContextMap()
	if got := ctx["request_id"]; got != "req-1" {
		t.Fatalf("expected request_id=req-1, got %v", got)
	}
	if got := ctx["trace_id"]; got != "0102030405060708090a0b0c0d0e0f10" {
		t.Fatalf("expected trace_id to be injected, got %v", got)
	}
}

func TestNewContextAndWrapContextAccumulateValues(t *testing.T) {
	t.Parallel()

	base, logs := newObservedLogger()
	ctx := NewContext(context.Background(), base, "user", "alice")
	ctx = WrapContext(ctx, "operation", "sync")

	FromContext(ctx).Info("wrapped logger")

	entries := logs.AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("expected 1 log entry, got %d", len(entries))
	}

	contextMap := entries[0].ContextMap()
	if got := contextMap["user"]; got != "alice" {
		t.Fatalf("expected user=alice, got %v", got)
	}
	if got := contextMap["operation"]; got != "sync" {
		t.Fatalf("expected operation=sync, got %v", got)
	}
	if got := contextMap["trace_id"]; got != "none" {
		t.Fatalf("expected trace_id=none when span is missing, got %v", got)
	}
}

func TestNewLogrToWriterWritesInfoMessage(t *testing.T) {
	t.Parallel()

	logger, logs := newObservedLogger()
	writer := NewLogrToWriter(logger)

	payload := []byte("worker output")
	n, err := writer.Write(payload)
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if n != len(payload) {
		t.Fatalf("expected %d bytes written, got %d", len(payload), n)
	}

	entries := logs.AllUntimed()
	if len(entries) != 1 {
		t.Fatalf("expected 1 log entry, got %d", len(entries))
	}
	if got := entries[0].Message; got != string(payload) {
		t.Fatalf("expected message %q, got %q", string(payload), got)
	}
}

func TestInitLoggerIncludesCallerAndStacktrace(t *testing.T) {
	t.Parallel()

	logPath := filepath.Join(t.TempDir(), "app.log")
	log := initLogger(config.Logger{
		IsJSON:   true,
		Filepath: logPath,
	})

	log.Error(errors.New("boom"), "failed")

	content, err := os.ReadFile(logPath)
	if err != nil {
		t.Fatalf("read log file: %v", err)
	}

	var entry map[string]any
	if err := json.Unmarshal(content, &entry); err != nil {
		t.Fatalf("unmarshal log entry %q: %v", string(content), err)
	}

	if _, ok := entry["caller"]; !ok {
		t.Fatalf("expected caller field in log entry: %v", entry)
	}
	if _, ok := entry["stacktrace"]; !ok {
		t.Fatalf("expected stacktrace field in log entry: %v", entry)
	}
}
