package services

import (
	"context"
	"encoding/json"
	"errors"
	"strings"
	"testing"
	"time"

	"github.com/cloudwego/eino/components/model"
	"github.com/cloudwego/eino/components/tool"
	"github.com/cloudwego/eino/schema"
	"gorm.io/gorm"
	"wa-assistant/backend/models"
)

// Database failures use a synthetic detail that must never enter tool output.
const knowledgePrivateDBError = "fixture-private-db-token customer-private@example.test"

func failKnowledgeQuery(t *testing.T, db *gorm.DB) func() {
	t.Helper()
	const name = "test:knowledge_unavailable"
	if err := db.Callback().Query().Before("gorm:query").Register(name, func(tx *gorm.DB) {
		if tx.Statement.Table == "knowledges" {
			tx.AddError(errors.New(knowledgePrivateDBError))
		}
	}); err != nil {
		t.Fatal(err)
	}
	remove := func() { db.Callback().Query().Remove(name) }
	t.Cleanup(remove)
	return remove
}

func invokeKnowledgeTestTool(t *testing.T, state *agentTurnState, agentID uint, ctx context.Context, name string) (*agentToolResult, error) {
	t.Helper()
	tools, err := makeAgentTools(agentID, ChatOptions{DryRun: true}, state)
	if err != nil {
		t.Fatal(err)
	}
	for _, base := range tools {
		info, err := base.Info(context.Background())
		if err != nil {
			t.Fatal(err)
		}
		if info.Name != name {
			continue
		}
		input := `{"query":"retur jaket"}`
		if name == "request_handoff" {
			input = `{"reason":"insufficient_evidence"}`
		}
		raw, err := base.(tool.InvokableTool).InvokableRun(ctx, input)
		if err != nil {
			return nil, err
		}
		if strings.Contains(raw, knowledgePrivateDBError) {
			t.Fatal("private database failure leaked into context")
		}
		var out agentToolResult
		if err := json.Unmarshal([]byte(raw), &out); err != nil {
			t.Fatal(err)
		}
		return &out, nil
	}
	t.Fatalf("tool missing: %s", name)
	return nil, nil
}

func TestKnowledgeLoadFailureDiffersFromEmptyResult(t *testing.T) {
	db := agenticTestDB(t)
	InvalidateKB(1)
	state := &agentTurnState{userMessage: "retur jaket"}
	remove := failKnowledgeQuery(t, db)
	out, err := invokeKnowledgeTestTool(t, state, 1, context.Background(), "search_knowledge")
	if err != nil || out.Issue != "knowledge_unavailable" || len(state.knowledge) != 0 {
		t.Fatalf("failed access reported as empty search: out=%+v err=%v", out, err)
	}
	remove()
	out, err = invokeKnowledgeTestTool(t, state, 1, context.Background(), "search_knowledge")
	if err != nil || out.Issue != "" || len(state.knowledge) != 0 {
		t.Fatalf("genuinely empty knowledge should be successful empty search: out=%+v err=%v", out, err)
	}
}

func TestKnowledgeLoadCancellationRejectsWarmCache(t *testing.T) {
	for _, expired := range []bool{false, true} {
		t.Run(map[bool]string{false: "canceled", true: "deadline"}[expired], func(t *testing.T) {
			db := agenticTestDB(t)
			k := models.Knowledge{AgentID: 1, Question: "Retur jaket", Answer: "Retur maksimal tujuh hari.", Active: true, ReviewStatus: "published"}
			if err := db.Create(&k).Error; err != nil {
				t.Fatal(err)
			}
			InvalidateKB(1)
			if len(KnowledgeFor(1)) != 1 {
				t.Fatal("failed warming fixture cache")
			}
			ctx, cancel := context.WithCancel(context.Background())
			want := context.Canceled
			if expired {
				cancel()
				ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(-time.Second))
				want = context.DeadlineExceeded
			}
			cancel()
			defer cancel()
			state := &agentTurnState{userMessage: "retur jaket"}
			out, err := invokeKnowledgeTestTool(t, state, 1, ctx, "search_knowledge")
			if !errors.Is(err, want) || out != nil || len(state.knowledge) != 0 {
				t.Fatalf("canceled tool consumed cached evidence: out=%+v err=%v", out, err)
			}
		})
	}
}

func TestKnowledgeLoadPublicationRaceIsNotEmpty(t *testing.T) {
	db := agenticTestDB(t)
	InvalidateKB(1)
	const name = "test:knowledge_publication"
	if err := db.Callback().Query().After("gorm:query").Register(name, func(tx *gorm.DB) {
		if tx.Statement.Table == "knowledges" {
			InvalidateKB(1)
		}
	}); err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Callback().Query().Remove(name) })
	out, err := invokeKnowledgeTestTool(t, &agentTurnState{userMessage: "retur jaket"}, 1, context.Background(), "search_knowledge")
	if err != nil || out.Issue != "knowledge_changed" {
		t.Fatalf("publication race presented as empty knowledge: out=%+v err=%v", out, err)
	}
}

func TestKnowledgeLoadRecoveryClearsFailureReason(t *testing.T) {
	db := agenticTestDB(t)
	k := models.Knowledge{AgentID: 1, Question: "Retur jaket", Answer: "Retur maksimal tujuh hari.", Active: true, ReviewStatus: "published"}
	if err := db.Create(&k).Error; err != nil {
		t.Fatal(err)
	}
	InvalidateKB(1)
	state := &agentTurnState{userMessage: "retur jaket"}
	remove := failKnowledgeQuery(t, db)
	if _, err := invokeKnowledgeTestTool(t, state, 1, context.Background(), "search_knowledge"); err != nil {
		t.Fatal(err)
	}
	remove()
	out, err := invokeKnowledgeTestTool(t, state, 1, context.Background(), "search_knowledge")
	if err != nil || out.Issue != "" || len(state.knowledge) != 1 || state.handoffReason != "" {
		t.Fatalf("successful recovery lost evidence: out=%+v err=%v", out, err)
	}
	if _, err := invokeKnowledgeTestTool(t, state, 1, context.Background(), "request_handoff"); err != nil {
		t.Fatal(err)
	}
	if state.handoffReason != "insufficient_evidence" {
		t.Fatalf("old infrastructure issue reused after successful retrieval: %s", state.handoffReason)
	}
}

type knowledgeFailureHandoffModel struct {
	scriptedAgentModel
	calls int
}

func (m *knowledgeFailureHandoffModel) WithTools([]*schema.ToolInfo) (model.ToolCallingChatModel, error) {
	return m, nil
}
func (m *knowledgeFailureHandoffModel) Generate(_ context.Context, in []*schema.Message, _ ...model.Option) (*schema.Message, error) {
	m.calls++
	name, args := "search_knowledge", `{"query":"retur jaket"}`
	if m.calls > 1 {
		name, args = "request_handoff", `{"reason":"insufficient_evidence"}`
		if !strings.Contains(in[len(in)-1].Content, "knowledge_unavailable") {
			return nil, errors.New("native transcript lost retrieval error")
		}
	}
	return schema.AssistantMessage("", []schema.ToolCall{{ID: name, Type: "function", Function: schema.FunctionCall{Name: name, Arguments: args}}}), nil
}

func TestKnowledgeLoadNativeHandoffPreservesAccessFailure(t *testing.T) {
	db := agenticTestDB(t)
	InvalidateKB(1)
	failKnowledgeQuery(t, db)
	state := &agentTurnState{userMessage: "retur jaket"}
	tools, err := makeAgentTools(1, ChatOptions{DryRun: true}, state)
	if err != nil {
		t.Fatal(err)
	}
	cm := &knowledgeFailureHandoffModel{}
	out, err := executeAnswerAgent(context.Background(), cm, tools, []*schema.Message{schema.UserMessage("retur jaket")}, state)
	if err != nil || out == nil || cm.calls != 2 || state.handoffReason != "knowledge_unavailable" {
		t.Fatalf("access failure mislabeled or workflow failed: calls=%d reason=%s err=%v", cm.calls, state.handoffReason, err)
	}
}

func TestKnowledgeLoadInvalidScopeRejected(t *testing.T) {
	agenticTestDB(t)
	out, err := invokeKnowledgeTestTool(t, &agentTurnState{userMessage: "retur jaket"}, 0, context.Background(), "search_knowledge")
	if err != nil || out.Issue != "knowledge_unavailable" {
		t.Fatalf("missing agent treated as successful search: out=%+v err=%v", out, err)
	}
}
