package metrics

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func TestTokenTrackingAuditWritesBoundedUTCJSONLWithPrivatePermissions(t *testing.T) {
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	require.NoError(t, os.WriteFile(path, nil, 0644))

	writer, err := openTokenTrackingAuditWriter(path, tokenTrackingAuditMaxBytes)
	require.NoError(t, err)

	rawCommand := strings.Repeat("界", tokenTrackingAuditMaxRawCommandBytes) + "tail"
	candidates := make([]TokenTrackingAuditSource, tokenTrackingAuditMaxCandidates+1)
	for i := range candidates {
		candidates[i] = TokenTrackingAuditSource{
			SourceKind: strings.Repeat("c", tokenTrackingAuditMaxSourceKindBytes+1),
			SourceName: strings.Repeat("s", tokenTrackingAuditMaxSourceNameBytes+1),
		}
	}
	allocations := make([]TokenTrackingAuditAllocation, tokenTrackingAuditMaxAllocations+1)
	for i := range allocations {
		allocations[i] = TokenTrackingAuditAllocation{
			SourceKind: strings.Repeat("a", tokenTrackingAuditMaxSourceKindBytes+1),
			SourceName: strings.Repeat("b", tokenTrackingAuditMaxSourceNameBytes+1),
			Input:      uint64(i + 1),
		}
	}
	writer.RecordTokenTrackingAudit(TokenTrackingAuditEntry{
		Timestamp:   time.Date(2026, 7, 28, 9, 30, 0, 123, time.FixedZone("UTC+8", 8*60*60)),
		Event:       strings.Repeat("e", tokenTrackingAuditMaxEventBytes+1),
		Runtime:     strings.Repeat("r", tokenTrackingAuditMaxRuntimeBytes+1),
		ToolName:    strings.Repeat("t", tokenTrackingAuditMaxToolNameBytes+1),
		RawCommand:  rawCommand,
		SourceKind:  strings.Repeat("k", tokenTrackingAuditMaxSourceKindBytes+1),
		SourceName:  strings.Repeat("n", tokenTrackingAuditMaxSourceNameBytes+1),
		Decision:    strings.Repeat("d", tokenTrackingAuditMaxDecisionBytes+1),
		Candidates:  candidates,
		Allocations: allocations,
	})
	require.NoError(t, writer.Close())

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

	data, err := os.ReadFile(path)
	require.NoError(t, err)
	lines := strings.Split(strings.TrimSuffix(string(data), "\n"), "\n")
	require.Len(t, lines, 1)
	require.LessOrEqual(t, len(lines[0])+1, tokenTrackingAuditMaxLineBytes)

	var got struct {
		SchemaVersion       int                            `json:"schemaVersion"`
		Timestamp           string                         `json:"timestamp"`
		Event               string                         `json:"event"`
		Runtime             string                         `json:"runtime"`
		ToolName            string                         `json:"toolName"`
		RawCommand          string                         `json:"rawCommand"`
		RawCommandTruncated bool                           `json:"rawCommandTruncated"`
		SourceKind          string                         `json:"sourceKind"`
		SourceName          string                         `json:"sourceName"`
		Decision            string                         `json:"decision"`
		Candidates          []TokenTrackingAuditSource     `json:"candidates"`
		Allocations         []TokenTrackingAuditAllocation `json:"allocations"`
	}
	require.NoError(t, json.Unmarshal([]byte(lines[0]), &got))
	require.Equal(t, tokenTrackingAuditSchemaVersion, got.SchemaVersion)
	require.Equal(t, "2026-07-28T01:30:00.000000123Z", got.Timestamp)
	require.Len(t, got.Event, tokenTrackingAuditMaxEventBytes)
	require.Len(t, got.Runtime, tokenTrackingAuditMaxRuntimeBytes)
	require.Len(t, got.ToolName, tokenTrackingAuditMaxToolNameBytes)
	require.LessOrEqual(t, len(got.RawCommand), tokenTrackingAuditMaxRawCommandBytes)
	require.True(t, got.RawCommandTruncated)
	require.True(t, strings.HasPrefix(rawCommand, got.RawCommand))
	require.Len(t, got.SourceKind, tokenTrackingAuditMaxSourceKindBytes)
	require.Len(t, got.SourceName, tokenTrackingAuditMaxSourceNameBytes)
	require.Len(t, got.Decision, tokenTrackingAuditMaxDecisionBytes)
	require.Len(t, got.Candidates, tokenTrackingAuditMaxCandidates)
	for _, candidate := range got.Candidates {
		require.Len(t, candidate.SourceKind, tokenTrackingAuditMaxSourceKindBytes)
		require.Len(t, candidate.SourceName, tokenTrackingAuditMaxSourceNameBytes)
	}
	require.Len(t, got.Allocations, tokenTrackingAuditMaxAllocations)
	for _, allocation := range got.Allocations {
		require.Len(t, allocation.SourceKind, tokenTrackingAuditMaxSourceKindBytes)
		require.Len(t, allocation.SourceName, tokenTrackingAuditMaxSourceNameBytes)
	}

	var keys map[string]json.RawMessage
	require.NoError(t, json.Unmarshal([]byte(lines[0]), &keys))
	for _, forbidden := range []string{
		"sessionID", "callID", "prompt", "output", "token", "credentials",
	} {
		require.NotContains(t, keys, forbidden)
	}
}

func TestTokenTrackingAuditRotatesOnlyPastExactBoundary(t *testing.T) {
	entry := TokenTrackingAuditEntry{
		Timestamp: time.Date(2026, 7, 28, 1, 2, 3, 0, time.UTC),
		Event:     "usage_source",
		Runtime:   "codex",
		Decision:  "exact-boundary",
	}
	lineBytes := tokenTrackingAuditLineSize(t, entry)
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)

	writer, err := openTokenTrackingAuditWriter(path, int64(lineBytes*2))
	require.NoError(t, err)
	writer.RecordTokenTrackingAudit(entry)
	writer.RecordTokenTrackingAudit(entry)
	_, err = os.Stat(path + ".1")
	require.ErrorIs(t, err, os.ErrNotExist, "an exact boundary must not rotate")

	writer.RecordTokenTrackingAudit(entry)
	require.NoError(t, writer.Close())

	active, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Len(t, active, lineBytes)
	backup, err := os.ReadFile(path + ".1")
	require.NoError(t, err)
	require.Len(t, backup, lineBytes*2)
}

func TestTokenTrackingAuditDoesNotRotateAnEmptyActiveFile(t *testing.T) {
	entry := TokenTrackingAuditEntry{
		Event:      "shell_source",
		RawCommand: strings.Repeat("x", 512),
	}
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	writer, err := openTokenTrackingAuditWriter(path, 1)
	require.NoError(t, err)

	writer.RecordTokenTrackingAudit(entry)
	require.NoError(t, writer.Close())

	active, err := os.ReadFile(path)
	require.NoError(t, err)
	require.NotEmpty(t, active)
	_, err = os.Stat(path + ".1")
	require.ErrorIs(t, err, os.ErrNotExist)
}

func TestTokenTrackingAuditKeepsOnlyThreeBackups(t *testing.T) {
	baseEntry := TokenTrackingAuditEntry{
		Timestamp: time.Date(2026, 7, 28, 1, 2, 3, 0, time.UTC),
		Event:     "usage_source",
		Runtime:   "claude",
		Decision:  "record-0",
	}
	lineBytes := tokenTrackingAuditLineSize(t, baseEntry)
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)

	writer, err := openTokenTrackingAuditWriter(path, int64(lineBytes))
	require.NoError(t, err)
	for i := 0; i < 6; i++ {
		entry := baseEntry
		entry.Decision = fmt.Sprintf("record-%d", i)
		writer.RecordTokenTrackingAudit(entry)
	}
	require.NoError(t, writer.Close())

	require.Equal(t, "record-5", readAuditDecision(t, path))
	require.Equal(t, "record-4", readAuditDecision(t, path+".1"))
	require.Equal(t, "record-3", readAuditDecision(t, path+".2"))
	require.Equal(t, "record-2", readAuditDecision(t, path+".3"))
	_, err = os.Stat(path + ".4")
	require.ErrorIs(t, err, os.ErrNotExist)
}

func TestTokenTrackingAuditConcurrentWritesRemainCompleteLines(t *testing.T) {
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	writer, err := openTokenTrackingAuditWriter(path, tokenTrackingAuditMaxBytes)
	require.NoError(t, err)

	const records = 200
	var wg sync.WaitGroup
	for i := 0; i < records; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			writer.RecordTokenTrackingAudit(TokenTrackingAuditEntry{
				Timestamp: time.Now(),
				Event:     "shell_source",
				Runtime:   "opencode",
				ToolName:  "shell",
				Decision:  fmt.Sprintf("record-%03d", i),
			})
		}(i)
	}
	wg.Wait()
	require.NoError(t, writer.Close())

	file, err := os.Open(path)
	require.NoError(t, err)
	defer file.Close()

	seen := make(map[string]struct{}, records)
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		var entry TokenTrackingAuditEntry
		require.NoError(t, json.Unmarshal(scanner.Bytes(), &entry))
		seen[entry.Decision] = struct{}{}
	}
	require.NoError(t, scanner.Err())
	require.Len(t, seen, records)
}

func TestTokenTrackingAuditCloseMakesLaterWritesNoOp(t *testing.T) {
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	writer, err := openTokenTrackingAuditWriter(path, tokenTrackingAuditMaxBytes)
	require.NoError(t, err)
	writer.RecordTokenTrackingAudit(TokenTrackingAuditEntry{
		Event:    "usage_source",
		Runtime:  "kimi",
		Decision: "before-close",
	})
	require.NoError(t, writer.Close())

	before, err := os.ReadFile(path)
	require.NoError(t, err)
	writer.RecordTokenTrackingAudit(TokenTrackingAuditEntry{
		Event:    "usage_source",
		Runtime:  "kimi",
		Decision: "after-close",
	})
	require.NoError(t, writer.Close())
	after, err := os.ReadFile(path)
	require.NoError(t, err)
	require.Equal(t, before, after)
}

func TestTokenTrackingAuditWriteFailureIsDiagnosticOnly(t *testing.T) {
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	writer, err := openTokenTrackingAuditWriter(path, tokenTrackingAuditMaxBytes)
	require.NoError(t, err)
	require.NoError(t, writer.file.Close())

	var logs bytes.Buffer
	previousLogger := slog.Default()
	slog.SetDefault(slog.New(slog.NewTextHandler(&logs, nil)))
	t.Cleanup(func() {
		slog.SetDefault(previousLogger)
	})

	rawCommand := "credential must not reach slog"
	require.NotPanics(t, func() {
		writer.RecordTokenTrackingAudit(TokenTrackingAuditEntry{
			Event:      "shell_source",
			RawCommand: rawCommand,
		})
	})
	require.NotContains(t, logs.String(), rawCommand)
	require.Contains(t, logs.String(), path)
	_ = writer.Close()
}

func tokenTrackingAuditLineSize(t *testing.T, entry TokenTrackingAuditEntry) int {
	t.Helper()
	path := filepath.Join(t.TempDir(), tokenTrackingAuditFilename)
	writer, err := openTokenTrackingAuditWriter(path, tokenTrackingAuditMaxBytes)
	require.NoError(t, err)
	writer.RecordTokenTrackingAudit(entry)
	require.NoError(t, writer.Close())
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	return len(data)
}

func readAuditDecision(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	require.NoError(t, err)
	var entry TokenTrackingAuditEntry
	require.NoError(t, json.Unmarshal(data, &entry))
	return entry.Decision
}
