package services

import (
	"context"
	"errors"
	"github.com/cloudwego/eino/schema"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"
	"wa-assistant/backend/models"
)

func TestSimulationLogStreamsBeforeCompletionAndSurvivesRestart(t *testing.T) {
	dir := t.TempDir()
	store := NewSimulationLogStore(dir)
	turnID, sessionID := NewSimulationID(), NewSimulationID()
	recorder, err := store.Start(1, turnID, sessionID, map[string]any{"question": "Apa syarat retur?"})
	if err != nil {
		t.Fatal(err)
	}
	ctx := recorder.Context(context.Background())
	RecordSimulationEvent(ctx, "tool_started", "search_knowledge", "running", map[string]any{"query": "retur"})
	live, err := store.Read(1, turnID)
	if err != nil || live.Status != "running" || len(live.Events) != 2 {
		t.Fatalf("live log: %+v, %v", live, err)
	}
	if !recorder.Finish("completed", map[string]any{"reply": "Retur maksimal 7 hari.", "response_validated": true}) {
		t.Fatal("not persisted")
	}
	restarted := NewSimulationLogStore(dir)
	complete, err := restarted.Read(1, turnID)
	if err != nil || complete.Status != "completed" || complete.SessionID != sessionID {
		t.Fatalf("restart: %+v, %v", complete, err)
	}
	if err := restarted.Feedback(1, turnID, "needs_work", "Syarat barang belum dipakai hilang.", 7); err != nil {
		t.Fatal(err)
	}
	complete, _ = restarted.Read(1, turnID)
	if complete.Events[len(complete.Events)-1].Stage != "feedback" || complete.Status != "completed" {
		t.Fatal("feedback changed result status")
	}
	if _, err := restarted.Read(2, turnID); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("cross-agent read: %v", err)
	}
	if err := restarted.Feedback(2, turnID, "correct", "", 7); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("cross-agent feedback: %v", err)
	}
	if _, err := store.Start(1, turnID, sessionID, nil); !errors.Is(err, os.ErrExist) {
		t.Fatalf("duplicate turn can be rerun: %v", err)
	}
	for _, bad := range []string{"../secret", "", "bad-id", strings.Repeat("a", 100)} {
		if _, err := store.Read(1, bad); err == nil {
			t.Fatal("invalid ID accepted")
		}
	}
	files, _ := filepath.Glob(filepath.Join(dir, "agent-1", "*.jsonl"))
	if len(files) != 1 {
		t.Fatalf("files: %v", files)
	}
	info, _ := os.Stat(files[0])
	if info.Mode().Perm() != 0600 {
		t.Fatalf("log mode: %v", info.Mode())
	}
}

func TestSimulationLogConcurrentEventsAreOrderedAndBounded(t *testing.T) {
	// This regression exercises clipping/redaction of explicitly enabled
	// diagnostic content. Default metadata privacy has separate coverage.
	t.Setenv("AI_SIMULATION_LOG_CONTENT", "true")
	store := NewSimulationLogStore(t.TempDir())
	id := NewSimulationID()
	r, err := store.Start(1, id, NewSimulationID(), nil)
	if err != nil {
		t.Fatal(err)
	}
	var wg sync.WaitGroup
	for i := 0; i < 12; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			r.Record("tool_completed", "check_answer", "rejected", map[string]any{"answer": strings.Repeat("x", 100000), "api_key": "must-never-be-stored", "nested": map[string]any{"authorization": "also-secret"}})
		}()
	}
	wg.Wait()
	r.Finish("completed", map[string]any{"reply": "Jawaban akhir"})
	log, err := store.Read(1, id)
	if err != nil || log.Status != "completed" {
		t.Fatalf("log: %v", err)
	}
	for i, event := range log.Events {
		if event.Seq != i+1 {
			t.Fatalf("non-monotonic seq: %d", event.Seq)
		}
	}
	raw, _ := os.ReadFile(filepath.Join(store.dir, "agent-1", id+".jsonl"))
	if len(raw) > simulationMaxBytes || strings.Contains(string(raw), "must-never-be-stored") || strings.Contains(string(raw), "also-secret") {
		t.Fatal("unbounded or secret log")
	}
	if !strings.Contains(string(raw), "truncated") {
		t.Fatal("clipped values must be explicit")
	}
	// Retention applies to reads too, even if no new test has started.
	old := time.Now().Add(-8 * 24 * time.Hour)
	if err := os.Chtimes(filepath.Join(store.dir, "agent-1", id+".jsonl"), old, old); err != nil {
		t.Fatal(err)
	}
	if _, err := store.Read(1, id); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("expired log: %v", err)
	}
}

func TestSimulationLoggingIsOptionalAndFailureIsVisible(t *testing.T) {
	RecordSimulationEvent(context.Background(), "tool_started", "search_knowledge", "running", nil)
	dir := filepath.Join(t.TempDir(), "not-a-directory")
	if err := os.WriteFile(dir, []byte("blocked"), 0600); err != nil {
		t.Fatal(err)
	}
	if _, err := NewSimulationLogStore(dir).Start(1, NewSimulationID(), NewSimulationID(), nil); err == nil {
		t.Fatal("write failure hidden")
	}
}

func TestSimulationRecordsNativeRepairWithoutPromptsOrReasoning(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Apa syarat retur?", Answer: "Retur maksimal 7 hari dan barang belum dipakai.", Active: true, ReviewStatus: "published", Scope: "general"}
	if err := db.Create(&k).Error; err != nil {
		t.Fatal(err)
	}
	InvalidateKB(1)
	store := NewSimulationLogStore(t.TempDir())
	id := NewSimulationID()
	r, err := store.Start(1, id, NewSimulationID(), map[string]any{"question": k.Question})
	if err != nil {
		t.Fatal(err)
	}
	s := &agentTurnState{reviewAnswer: approveTestAnswer, userMessage: k.Question, policy: responsePolicyForLength(selectAIResponsePolicy(k.Question, "", "", 1), "short", k.Question)}
	tools, err := makeAgentTools(1, ChatOptions{DryRun: true}, s)
	if err != nil {
		t.Fatal(err)
	}
	cm := &observedChatModel{inner: &answerLoopModel{sourceID: k.ID}, modelName: "fixture"}
	out, err := executeEino(r.Context(context.Background()), cm, tools, []*schema.Message{schema.SystemMessage("private-prompt-never-logged"), schema.UserMessage(k.Question)})
	if err != nil {
		t.Fatal(err)
	}
	r.Finish("completed", map[string]any{"reply": out.Content})
	record, err := store.Read(1, id)
	if err != nil {
		t.Fatal(err)
	}
	rejected, corrected := false, false
	for _, e := range record.Events {
		if e.Name == "check_answer" && e.Status == "rejected" && e.Data["issue"] == "unsupported_numbers" {
			rejected = true
		}
		if rejected && e.Name == "check_answer" && e.Stage == "tool_completed" && e.Status == "completed" {
			corrected = true
		}
	}
	if !rejected || !corrected {
		t.Fatal("intermediate rejection lost after correction")
	}
	raw, _ := os.ReadFile(filepath.Join(store.dir, "agent-1", id+".jsonl"))
	if strings.Contains(string(raw), "private-prompt-never-logged") {
		t.Fatal("private model input leaked")
	}
}
