package audit

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

func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) }

// newBufferLogger yazılan satırları incelemek için bellek içi logger döndürür.
func newBufferLogger(t *testing.T) (*Logger, *bytes.Buffer) {
	t.Helper()
	var buf bytes.Buffer
	l := &Logger{w: &buf, log: testLogger(), now: func() time.Time { return time.Unix(1700000000, 0).UTC() }}
	return l, &buf
}

// decodeLines JSON satırlarını Entry'ye çevirir.
func decodeLines(t *testing.T, buf *bytes.Buffer) []Entry {
	t.Helper()
	var out []Entry
	for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") {
		if line == "" {
			continue
		}
		var e Entry
		if err := json.Unmarshal([]byte(line), &e); err != nil {
			t.Fatalf("satır JSON değil: %v (%q)", err, line)
		}
		out = append(out, e)
	}
	return out
}

// Kayıtlar birer JSON satırı olmalı (makine + insan okur).
func TestRecord_WritesJSONLines(t *testing.T) {
	l, buf := newBufferLogger(t)

	l.Record(Entry{Actor: "user", UserID: "u1", ConversationID: "c1", Action: "message.send", Detail: "merhaba"})
	l.Record(Entry{Actor: "agent", Action: "llm.generate"})

	entries := decodeLines(t, buf)
	if len(entries) != 2 {
		t.Fatalf("2 kayıt bekleniyordu, %d", len(entries))
	}
	if entries[0].Action != "message.send" || entries[0].UserID != "u1" {
		t.Errorf("ilk kayıt: %+v", entries[0])
	}
	if entries[0].Result != ResultSuccess {
		t.Errorf("varsayılan sonuç success olmalı, %q", entries[0].Result)
	}
}

// Zorunlu alanlar otomatik atanmalı: ID, zaman damgası, sonuç, aktör.
func TestRecord_FillsDefaults(t *testing.T) {
	l, buf := newBufferLogger(t)
	l.Record(Entry{Action: "x"})

	e := decodeLines(t, buf)[0]
	if e.ID == "" {
		t.Error("ID atanmadı")
	}
	if e.At.IsZero() {
		t.Error("At atanmadı")
	}
	if e.Result != ResultSuccess {
		t.Errorf("Result = %q", e.Result)
	}
	if e.Actor != "system" {
		t.Errorf("Actor = %q, beklenen system", e.Actor)
	}
	if !strings.HasPrefix(e.ID, "aud_") {
		t.Errorf("ID ön eki = %q, beklenen aud_", e.ID)
	}
}

// Verilen alanlar EZİLMEMELİ (sonuç: denied kaydı success'e dönüşmemeli).
func TestRecord_PreservesExplicitFields(t *testing.T) {
	l, buf := newBufferLogger(t)
	l.Record(Entry{Action: "tool.exec", Result: ResultDenied, Target: "CATIA.CreatePlane", Meta: map[string]any{"risk": "HIGH"}})

	e := decodeLines(t, buf)[0]
	if e.Result != ResultDenied {
		t.Errorf("Result = %q, beklenen denied", e.Result)
	}
	if e.Target != "CATIA.CreatePlane" {
		t.Errorf("Target = %q", e.Target)
	}
	if e.Meta["risk"] != "HIGH" {
		t.Errorf("Meta = %v", e.Meta)
	}
}

// Yazma ASLA üretim akışını durdurmamalı.
func TestRecord_WriteErrorDoesNotPanic(t *testing.T) {
	l := &Logger{w: failWriter{}, log: testLogger(), now: time.Now}

	l.Record(Entry{Action: "x"})
	l.Success("u", "c", "y", "detay")
}

type failWriter struct{}

func (failWriter) Write([]byte) (int, error) { return 0, os.ErrPermission }

// Denetim kaydı hassas olabilir; dosya sahibine özel açılır (0600).
// NOT: Windows'ta POSIX izin bitleri uygulanmaz (NTFS ACL devreye girer);
// bu yüzden test yalnızca Unix-benzeri platformlarda izin denetler.
func TestNew_FilePermissions(t *testing.T) {
	if runtime.GOOS == "windows" {
		t.Skip("Windows dosya izinleri POSIX 0600 uygulamaz")
	}
	path := filepath.Join(t.TempDir(), "audit.log")
	l, err := New(Options{Path: path, Logger: testLogger()})
	if err != nil {
		t.Fatalf("New: %v", err)
	}
	defer l.Close()

	l.Record(Entry{Action: "x"})

	info, err := os.Stat(path)
	if err != nil {
		t.Fatalf("dosya oluşmadı: %v", err)
	}
	if perm := info.Mode().Perm(); perm != 0o600 {
		t.Errorf("dosya izni = %o, beklenen 600", perm)
	}
}

// Append modunda açılmalı: yeniden başlatınca kayıtlar EZİLMEMELİ.
func TestNew_AppendsOnRestart(t *testing.T) {
	path := filepath.Join(t.TempDir(), "audit.log")

	l1, err := New(Options{Path: path, Logger: testLogger()})
	if err != nil {
		t.Fatal(err)
	}
	l1.Record(Entry{Action: "birinci"})
	l1.Close()

	l2, err := New(Options{Path: path, Logger: testLogger()})
	if err != nil {
		t.Fatal(err)
	}
	l2.Record(Entry{Action: "ikinci"})
	l2.Close()

	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatal(err)
	}
	lines := strings.Split(strings.TrimSpace(string(data)), "\n")
	if len(lines) != 2 {
		t.Fatalf("kayıtlar üstüne yazılmamalıydı, %d satır", len(lines))
	}
}

// Dosya açılamazsa HATA DÖNMELİ (sessizce stdout'a düşmek yanıltıcı olurdu).
func TestNew_UnopenablePathFails(t *testing.T) {
	if _, err := New(Options{Path: t.TempDir(), Logger: testLogger()}); err == nil {
		t.Fatal("geçersiz yol için hata bekleniyordu")
	}
}

// Yardımcı kayıt fonksiyonları doğru sonucu yazmalı.
func TestHelpers(t *testing.T) {
	l, buf := newBufferLogger(t)

	l.Success("u1", "c1", "konusma.sil", "ok")
	l.Failure("u1", "c1", "konusma.sil", "yok")
	l.Denied("u1", "tool.exec", "yetki yok")

	entries := decodeLines(t, buf)
	if len(entries) != 3 {
		t.Fatalf("3 kayıt bekleniyordu: %d", len(entries))
	}
	want := []string{ResultSuccess, ResultError, ResultDenied}
	for i, w := range want {
		if entries[i].Result != w {
			t.Errorf("%d. kayıt sonucu = %q, beklenen %q", i, entries[i].Result, w)
		}
	}
	if entries[0].Actor != "user" {
		t.Errorf("Success aktörü = %q", entries[0].Actor)
	}
	if entries[2].Actor != "system" {
		t.Errorf("Denied aktörü = %q (sistem kararı)", entries[2].Actor)
	}
}

// Denetim kaydı tam içerik TUTMAMALI: kullanıcı metni özetlenir.
// Kesme rune tabanlıdır: Türkçe karakterler bozulmamalı (geçersiz UTF-8 kayıt demektir).
func TestSanitize(t *testing.T) {
	cases := []struct {
		in   string
		max  int
		want string
	}{
		{"  merhaba  ", 100, "merhaba"},
		{"satır1\nsatır2", 100, "satır1 satır2"},
		{"çok uzun bir metin", 12, "çok uzun ..."},
		{"kısa", 0, "kısa"}, // max=0 → sınırlama yok
		{"uzun", 2, "..."},  // max < "..." uzunluğu → yalnızca ayırıcı
	}
	for _, c := range cases {
		got := Sanitize(c.in, c.max)
		if got != c.want {
			t.Errorf("Sanitize(%q, %d) = %q, beklenen %q", c.in, c.max, got, c.want)
		}
	}

	// Türkçe karakterlerde kesme bozmamalı: sonuç geçerli UTF-8 olmalı.
	out := Sanitize("şğüöçİı etiketlerle dolu bir cümle", 10)
	if !utf8.ValidString(out) {
		t.Errorf("kesme geçersiz UTF-8 üretti: %q", out)
	}
	if !strings.HasPrefix(out, "şğüöç") {
		t.Errorf("kesme karakterleri bozdu: %q", out)
	}
}

// Eşzamanlı kayıtlar karışmamalı: her satır geçerli JSON olmalı.
func TestRecord_Concurrent(t *testing.T) {
	l, buf := newBufferLogger(t)

	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for j := 0; j < 50; j++ {
				l.Record(Entry{Actor: "user", Action: "islem", Detail: "x"})
			}
		}()
	}
	wg.Wait()

	// Yazma mutex altında olmalı: 400 geçerli satır beklenir.
	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
	if len(lines) != 400 {
		t.Fatalf("400 satır bekleniyordu: %d", len(lines))
	}
	for i, line := range lines {
		var e Entry
		if err := json.Unmarshal([]byte(line), &e); err != nil {
			t.Fatalf("%d. satır geçersiz (kayıtlar karışmış): %v", i, err)
		}
	}
}

// Close sonrası kayıt çökmemeli (kayıp sessizce olsa da akış durmamalı).
func TestRecord_AfterClose(t *testing.T) {
	path := filepath.Join(t.TempDir(), "audit.log")
	l, err := New(Options{Path: path, Logger: testLogger()})
	if err != nil {
		t.Fatal(err)
	}
	if err := l.Close(); err != nil {
		t.Fatalf("Close: %v", err)
	}
	if err := l.Close(); err != nil {
		t.Fatalf("ikinci Close güvenli olmalı: %v", err)
	}
	l.Record(Entry{Action: "kapandıktan sonra"})
}
