package logging

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"log/slog"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"go.opentelemetry.io/otel/trace"
)

type failingHandlerWriter struct {
	err error
}

func (writer failingHandlerWriter) Write([]byte) (int, error) {
	return 0, writer.err
}

func TestWrapHandlerAddsRequestID(t *testing.T) {
	var buffer bytes.Buffer
	logger := slog.New(WrapHandler(slog.NewJSONHandler(&buffer, nil)))

	logger.InfoContext(WithRequestID(context.Background(), "req_123"), "handled request")

	var entry map[string]any
	require.NoError(t, json.Unmarshal(buffer.Bytes(), &entry))
	require.Equal(t, "req_123", entry["request_id"])
	require.Equal(t, "handled request", entry["msg"])
}

func TestWrapHandlerOmitsEmptyRequestID(t *testing.T) {
	var buffer bytes.Buffer
	logger := slog.New(WrapHandler(slog.NewJSONHandler(&buffer, nil)))

	logger.InfoContext(context.Background(), "handled request")

	var entry map[string]any
	require.NoError(t, json.Unmarshal(buffer.Bytes(), &entry))
	require.NotContains(t, entry, "request_id")
}

func TestWrapHandlerAddsTraceContext(t *testing.T) {
	var buffer bytes.Buffer
	logger := slog.New(WrapHandler(slog.NewJSONHandler(&buffer, nil)))
	traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736")
	require.NoError(t, err)
	spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7")
	require.NoError(t, err)
	ctx := trace.ContextWithSpanContext(
		WithRequestID(context.Background(), "req_trace"),
		trace.NewSpanContext(trace.SpanContextConfig{
			TraceID:    traceID,
			SpanID:     spanID,
			TraceFlags: trace.FlagsSampled,
		}),
	)

	logger.InfoContext(ctx, "handled request")

	var entry map[string]any
	require.NoError(t, json.Unmarshal(buffer.Bytes(), &entry))
	require.Equal(t, "req_trace", entry["request_id"])
	require.Equal(t, "4bf92f3577b34da6a3ce929d0e0e4736", entry["trace_id"])
	require.Equal(t, "00f067aa0ba902b7", entry["span_id"])
}

func TestWrapHandlerPreservesWithAttrsAndWithGroup(t *testing.T) {
	var buffer bytes.Buffer
	logger := slog.New(WrapHandler(slog.NewJSONHandler(&buffer, nil))).With("component", "router").WithGroup("http")

	logger.InfoContext(WithRequestID(context.Background(), "req_group"), "handled request", "status", 200)

	var entry map[string]any
	require.NoError(t, json.Unmarshal(buffer.Bytes(), &entry))
	require.Equal(t, "router", entry["component"])
	httpGroup, ok := entry["http"].(map[string]any)
	require.True(t, ok)
	require.Equal(t, "req_group", httpGroup["request_id"])
	require.Equal(t, float64(200), httpGroup["status"])
}

func TestWrapHandlerPreservesFixedDomainAndComponent(t *testing.T) {
	var buffer bytes.Buffer
	logger := slog.New(WrapHandler(slog.NewJSONHandler(&buffer, nil))).With(
		slog.String("domain", "message"),
		slog.String("component", "delivery_fanout"),
	)

	logger.Warn("delivery failed")

	var entry map[string]any
	require.NoError(t, json.Unmarshal(buffer.Bytes(), &entry))
	require.Equal(t, "message", entry["domain"])
	require.Equal(t, "delivery_fanout", entry["component"])
	require.Equal(t, 1, bytes.Count(buffer.Bytes(), []byte(`"domain"`)))
}

func TestWrapHandlerPropagatesWriteAndErrorFlushFailures(t *testing.T) {
	writeErr := errors.New("write failed")
	flushErr := errors.New("flush failed")
	flushCalls := 0
	handler := WrapHandler(slog.NewJSONHandler(failingHandlerWriter{err: writeErr}, nil), func() error {
		flushCalls++
		return flushErr
	})
	record := slog.NewRecord(time.Now(), slog.LevelError, "failed", 0)

	err := handler.Handle(context.Background(), record)

	require.ErrorIs(t, err, writeErr)
	require.ErrorIs(t, err, flushErr)
	require.Equal(t, 1, flushCalls)
}

func TestWrapHandlerDoesNotFlushBelowError(t *testing.T) {
	writeErr := errors.New("write failed")
	flushCalls := 0
	handler := WrapHandler(slog.NewJSONHandler(failingHandlerWriter{err: writeErr}, nil), func() error {
		flushCalls++
		return nil
	})
	record := slog.NewRecord(time.Now(), slog.LevelInfo, "event", 0)

	err := handler.Handle(context.Background(), record)

	require.ErrorIs(t, err, writeErr)
	require.Zero(t, flushCalls)
}

func TestWrapHandlerDisabledLevelSkipsWriteAndFlush(t *testing.T) {
	var output bytes.Buffer
	flushCalls := 0
	handler := WrapHandler(slog.NewJSONHandler(&output, &slog.HandlerOptions{Level: slog.LevelError}), func() error {
		flushCalls++
		return nil
	})
	logger := slog.New(handler)

	logger.Debug("disabled")

	require.Empty(t, output.String())
	require.Zero(t, flushCalls)
}
