package logging

import (
	"bytes"
	"encoding/json"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
	"github.com/tidbcloud/AgentTeam/internal/config"
)

func TestPipelineDefaultsDomainAndAddsBoundedErrorStack(t *testing.T) {
	var output bytes.Buffer
	pipeline, err := Open(config.DefaultLogConfig(), &output)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, pipeline.Close()) })

	pipeline.Logger().Info("ready")
	pipeline.Logger().With("domain", "kb").Error("mirror failed")

	lines := strings.Split(strings.TrimSpace(output.String()), "\n")
	require.Len(t, lines, 2)
	first := decodeLogLine(t, lines[0])
	second := decodeLogLine(t, lines[1])
	require.Equal(t, "system", first["domain"])
	require.NotContains(t, first, "stacktrace")
	require.Equal(t, "kb", second["domain"])
	stack, ok := second["stacktrace"].(string)
	require.True(t, ok)
	require.NotEmpty(t, stack)
	require.LessOrEqual(t, len(stack), stacktraceMaxBytes)
}

func TestPipelinePreservesExplicitErrorStack(t *testing.T) {
	var output bytes.Buffer
	pipeline, err := Open(config.DefaultLogConfig(), &output)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, pipeline.Close()) })

	pipeline.Logger().Error("failed", slog.String("stacktrace", "trusted stack"))

	entry := decodeLogLine(t, strings.TrimSpace(output.String()))
	require.Equal(t, "trusted stack", entry["stacktrace"])
}

func TestPipelineFileBufferFlushAndPermissions(t *testing.T) {
	path := filepath.Join(t.TempDir(), "private", "agentteam.log")
	cfg := fileLogConfig(path)
	pipeline, err := Open(cfg, nil)
	require.NoError(t, err)

	pipeline.Logger().Info("buffered")
	contents, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Empty(t, contents)
	require.NoError(t, pipeline.Flush())
	contents, err = os.ReadFile(path)
	require.NoError(t, err)
	require.Contains(t, string(contents), `"msg":"buffered"`)
	require.NoError(t, pipeline.Close())

	info, err := os.Stat(path)
	require.NoError(t, err)
	require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
}

func TestPipelineErrorFlushesBufferedFile(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.log")
	pipeline, err := Open(fileLogConfig(path), nil)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, pipeline.Close()) })

	pipeline.Logger().Error("failed")
	contents, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Contains(t, string(contents), `"msg":"failed"`)
}

func TestPipelineBothEncodesOncePerSink(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.log")
	cfg := fileLogConfig(path)
	cfg.Output = config.LogOutputBoth
	var stdout bytes.Buffer
	pipeline, err := Open(cfg, &stdout)
	require.NoError(t, err)

	pipeline.Logger().Info("ready")
	require.NoError(t, pipeline.Close())
	fileContents, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Equal(t, stdout.String(), string(fileContents))
	require.Equal(t, 1, strings.Count(stdout.String(), `"msg":"ready"`))
}

func TestPipelineBothDeduplicatesStdoutTargetingLogFile(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.log")
	stdout, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, stdout.Close()) })
	cfg := fileLogConfig(path)
	cfg.Output = config.LogOutputBoth
	pipeline, err := Open(cfg, stdout)
	require.NoError(t, err)

	pipeline.Logger().Info("ready")
	require.NoError(t, pipeline.Close())
	contents, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Equal(t, 1, strings.Count(string(contents), `"msg":"ready"`))
}

func TestPipelineRejectsUncreatableFileParent(t *testing.T) {
	parent := filepath.Join(t.TempDir(), "not-a-directory")
	require.NoError(t, os.WriteFile(parent, []byte("x"), 0o600))
	_, err := Open(fileLogConfig(filepath.Join(parent, "agentteam.log")), nil)
	require.ErrorContains(t, err, "create log directory")
}

func TestPipelineRejectsParentWithoutRotatePermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("POSIX directory permissions required")
	}
	if os.Geteuid() == 0 {
		t.Skip("root can rotate files in a directory without write mode bits")
	}
	directory := t.TempDir()
	path := filepath.Join(directory, "agentteam.log")
	require.NoError(t, os.WriteFile(path, []byte("existing"), 0o600))
	require.NoError(t, os.Chmod(directory, 0o500))
	t.Cleanup(func() { _ = os.Chmod(directory, 0o700) })

	_, err := Open(fileLogConfig(path), nil)

	require.ErrorContains(t, err, "create log rotation probe")
}

func TestPipelinePeriodicFlush(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.log")
	cfg := fileLogConfig(path)
	cfg.FlushInterval = 5 * time.Millisecond
	pipeline, err := Open(cfg, nil)
	require.NoError(t, err)
	t.Cleanup(func() { require.NoError(t, pipeline.Close()) })

	pipeline.Logger().Info("eventually visible")
	require.Eventually(t, func() bool {
		contents, readErr := os.ReadFile(path)
		return readErr == nil && strings.Contains(string(contents), `"msg":"eventually visible"`)
	}, time.Second, 10*time.Millisecond)
}

func TestPipelineCompletesCompressionAndRetentionBeforeCloseReturns(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.log")
	cfg := fileLogConfig(path)
	cfg.MaxSizeMB = 1
	cfg.MaxDays = 0
	cfg.MaxBackups = 1
	cfg.BufferSizeKB = 0
	pipeline, err := Open(cfg, nil)
	require.NoError(t, err)

	payload := strings.Repeat("x", 700*1024)
	for range 3 {
		pipeline.Logger().Info("large event", "payload", payload)
	}
	require.NoError(t, pipeline.Close())

	compressed, err := filepath.Glob(filepath.Join(filepath.Dir(path), "agentteam-*.log.gz"))
	require.NoError(t, err)
	require.Len(t, compressed, 1)
	temporary, err := filepath.Glob(filepath.Join(filepath.Dir(path), "*.tmp"))
	require.NoError(t, err)
	require.Empty(t, temporary)
}

func TestPipelineRetainsBoundedBackupsWhenActivePathEndsWithGzipExtension(t *testing.T) {
	path := filepath.Join(t.TempDir(), "agentteam.gz")
	cfg := fileLogConfig(path)
	cfg.MaxSizeMB = 1
	cfg.MaxDays = 0
	cfg.MaxBackups = 1
	cfg.Compression = config.LogCompressionNone
	cfg.BufferSizeKB = 0
	pipeline, err := Open(cfg, nil)
	require.NoError(t, err)

	payload := strings.Repeat("x", 700*1024)
	for range 3 {
		pipeline.Logger().Info("large event", "payload", payload)
	}
	require.NoError(t, pipeline.Close())

	backups, err := filepath.Glob(filepath.Join(filepath.Dir(path), "agentteam-*.gz"))
	require.NoError(t, err)
	require.Len(t, backups, 1)
}

func fileLogConfig(path string) config.LogConfig {
	cfg := config.DefaultLogConfig()
	cfg.Output = config.LogOutputFile
	cfg.FilePath = path
	return cfg
}

func decodeLogLine(t *testing.T, line string) map[string]any {
	t.Helper()
	var decoded map[string]any
	require.NoError(t, json.Unmarshal([]byte(line), &decoded))
	return decoded
}

func BenchmarkPipelineDisabledLevel(b *testing.B) {
	pipeline, err := Open(config.DefaultLogConfig(), io.Discard)
	if err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = pipeline.Close() })
	logger := pipeline.Logger().With("domain", "kb")
	b.ReportAllocs()
	b.ResetTimer()
	for range b.N {
		logger.Debug("event")
	}
}

func BenchmarkPipelineInfoWithDomain(b *testing.B) {
	pipeline, err := Open(config.DefaultLogConfig(), io.Discard)
	if err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = pipeline.Close() })
	logger := pipeline.Logger().With("domain", "kb")
	b.ReportAllocs()
	b.ResetTimer()
	for range b.N {
		logger.Info("event")
	}
}

func BenchmarkPipelineErrorWithBoundedStack(b *testing.B) {
	pipeline, err := Open(config.DefaultLogConfig(), io.Discard)
	if err != nil {
		b.Fatal(err)
	}
	b.Cleanup(func() { _ = pipeline.Close() })
	logger := pipeline.Logger().With("domain", "kb")
	b.ReportAllocs()
	b.ResetTimer()
	for range b.N {
		logger.Error("event")
	}
}
