package builtin

import (
	"context"
	"log/slog"
	"strings"
	"testing"

	adapters "github.com/memohai/memoh/internal/memory/adapters"
)

// fakeLLM implements adapters.LLM for testing the formation pipeline.
type fakeLLM struct {
	extractFacts  []string
	extractErr    error
	decideActions []adapters.DecisionAction
	decideErr     error
	extractCalls  int
	decideCalls   int
}

func (f *fakeLLM) Extract(_ context.Context, _ adapters.ExtractRequest) (adapters.ExtractResponse, error) {
	f.extractCalls++
	return adapters.ExtractResponse{Facts: f.extractFacts}, f.extractErr
}

func (f *fakeLLM) Decide(_ context.Context, _ adapters.DecideRequest) (adapters.DecideResponse, error) {
	f.decideCalls++
	return adapters.DecideResponse{Actions: f.decideActions}, f.decideErr
}

func (*fakeLLM) Compact(context.Context, adapters.CompactRequest) (adapters.CompactResponse, error) {
	return adapters.CompactResponse{}, nil
}

func (*fakeLLM) DetectLanguage(context.Context, string) (string, error) {
	return "", nil
}

func TestFormationExtractAndAdd(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}
	llm := &fakeLLM{
		extractFacts: []string{"User likes oolong tea", "User is based in Berlin"},
		decideActions: []adapters.DecisionAction{
			{Event: "ADD", Text: "User likes oolong tea"},
			{Event: "ADD", Text: "User is based in Berlin"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I like oolong tea and I live in Berlin"},
			{Role: "assistant", Content: "Noted!"},
		},
	})

	if result.ExtractedFacts != 2 {
		t.Fatalf("expected 2 extracted facts, got %d", result.ExtractedFacts)
	}
	if result.Added != 2 {
		t.Fatalf("expected 2 adds, got %d", result.Added)
	}
	if result.Updated != 0 || result.Deleted != 0 {
		t.Fatalf("expected no updates/deletes, got updated=%d deleted=%d", result.Updated, result.Deleted)
	}
	if len(store.items) != 2 {
		t.Fatalf("expected 2 items in store, got %d", len(store.items))
	}
	if llm.extractCalls != 1 || llm.decideCalls != 1 {
		t.Fatalf("expected 1 extract + 1 decide call, got %d/%d", llm.extractCalls, llm.decideCalls)
	}
}

func TestFormationUpdate(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	addResp, err := runtime.Add(context.Background(), adapters.AddRequest{
		BotID:   "bot-1",
		Message: "User lives in Tokyo",
		Filters: map[string]any{"bot_id": "bot-1"},
	})
	if err != nil {
		t.Fatalf("seed Add failed: %v", err)
	}
	memID := addResp.Results[0].ID

	llm := &fakeLLM{
		extractFacts: []string{"User moved to Berlin"},
		decideActions: []adapters.DecisionAction{
			{Event: "UPDATE", ID: memID, Text: "User is based in Berlin", OldMemory: "User lives in Tokyo"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "Actually, I moved to Berlin"},
		},
	})

	if result.Updated != 1 {
		t.Fatalf("expected 1 update, got %d", result.Updated)
	}
	if result.Added != 0 {
		t.Fatalf("expected 0 adds, got %d", result.Added)
	}

	item, ok := store.items[memID]
	if !ok {
		t.Fatalf("expected memory %q to still exist", memID)
	}
	if !strings.Contains(item.Memory, "Berlin") {
		t.Fatalf("expected updated memory to contain Berlin, got %q", item.Memory)
	}
}

func TestFormationDelete(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	addResp, err := runtime.Add(context.Background(), adapters.AddRequest{
		BotID:   "bot-1",
		Message: "User likes coffee",
		Filters: map[string]any{"bot_id": "bot-1"},
	})
	if err != nil {
		t.Fatalf("seed Add failed: %v", err)
	}
	memID := addResp.Results[0].ID

	llm := &fakeLLM{
		extractFacts: []string{"User no longer drinks coffee"},
		decideActions: []adapters.DecisionAction{
			{Event: "DELETE", ID: memID},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I stopped drinking coffee"},
		},
	})

	if result.Deleted != 1 {
		t.Fatalf("expected 1 delete, got %d", result.Deleted)
	}
	if _, ok := store.items[memID]; ok {
		t.Fatal("expected memory to be deleted from store")
	}
}

func TestFormationNOOP(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	llm := &fakeLLM{
		extractFacts: []string{"User likes tea"},
		decideActions: []adapters.DecisionAction{
			{Event: "NOOP"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I like tea"},
		},
	})

	if result.Skipped != 1 {
		t.Fatalf("expected 1 skipped, got %d", result.Skipped)
	}
	if result.Added != 0 || result.Updated != 0 || result.Deleted != 0 {
		t.Fatalf("expected no mutations, got added=%d updated=%d deleted=%d", result.Added, result.Updated, result.Deleted)
	}
	if len(store.items) != 0 {
		t.Fatalf("expected 0 items in store, got %d", len(store.items))
	}
}

func TestFormationNoFacts(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	llm := &fakeLLM{
		extractFacts: []string{},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "Hello"},
			{Role: "assistant", Content: "Hi there!"},
		},
	})

	if result.ExtractedFacts != 0 {
		t.Fatalf("expected 0 extracted facts, got %d", result.ExtractedFacts)
	}
	if llm.decideCalls != 0 {
		t.Fatal("expected Decide to NOT be called when no facts extracted")
	}
}

func TestFormationMixedActions(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	addResp, _ := runtime.Add(context.Background(), adapters.AddRequest{
		BotID:   "bot-1",
		Message: "User lives in Tokyo",
		Filters: map[string]any{"bot_id": "bot-1"},
	})
	existingID := addResp.Results[0].ID

	llm := &fakeLLM{
		extractFacts: []string{"User moved to Berlin", "User prefers dark mode"},
		decideActions: []adapters.DecisionAction{
			{Event: "UPDATE", ID: existingID, Text: "User lives in Berlin"},
			{Event: "ADD", Text: "User prefers dark mode"},
			{Event: "NOOP"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I moved to Berlin and I like dark mode"},
		},
	})

	if result.Added != 1 {
		t.Fatalf("expected 1 add, got %d", result.Added)
	}
	if result.Updated != 1 {
		t.Fatalf("expected 1 update, got %d", result.Updated)
	}
	if result.Skipped != 1 {
		t.Fatalf("expected 1 skipped, got %d", result.Skipped)
	}
	if len(store.items) != 2 {
		t.Fatalf("expected 2 items in store, got %d", len(store.items))
	}
}

func TestFormationInvalidActionsSkipped(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	llm := &fakeLLM{
		extractFacts: []string{"User likes cats"},
		decideActions: []adapters.DecisionAction{
			{Event: "ADD", Text: ""},
			{Event: "UPDATE", ID: "", Text: "something"},
			{Event: "DELETE", ID: ""},
			{Event: "UNKNOWN_EVENT", Text: "foo"},
			{Event: "ADD", Text: "User likes cats"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I like cats"},
		},
	})

	if result.Added != 1 {
		t.Fatalf("expected 1 valid add, got %d", result.Added)
	}
	if result.Skipped != 4 {
		t.Fatalf("expected 4 skipped (3 invalid + 1 unknown), got %d", result.Skipped)
	}
}

func TestFormationDuplicateActionsSameID(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	addResp, _ := runtime.Add(context.Background(), adapters.AddRequest{
		BotID:   "bot-1",
		Message: "User likes tea",
		Filters: map[string]any{"bot_id": "bot-1"},
	})
	memID := addResp.Results[0].ID

	llm := &fakeLLM{
		extractFacts: []string{"Updated fact"},
		decideActions: []adapters.DecisionAction{
			{Event: "UPDATE", ID: memID, Text: "User prefers coffee"},
			{Event: "UPDATE", ID: memID, Text: "User prefers juice"},
		},
	}

	result := runFormation(context.Background(), slog.Default(), llm, runtime, adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I changed my mind"},
		},
	})

	if result.Updated != 1 {
		t.Fatalf("expected 1 update (second should be deduped), got %d", result.Updated)
	}
	if result.Skipped != 1 {
		t.Fatalf("expected 1 skipped (duplicate), got %d", result.Skipped)
	}
}

func TestOnAfterChatWithLLM(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}
	llm := &fakeLLM{
		extractFacts: []string{"User prefers dark mode"},
		decideActions: []adapters.DecisionAction{
			{Event: "ADD", Text: "User prefers dark mode"},
		},
	}

	p := NewBuiltinProvider(slog.Default(), runtime, nil, nil)
	p.SetLLM(llm)

	err := p.OnAfterChat(context.Background(), adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I prefer dark mode"},
			{Role: "assistant", Content: "Got it!"},
		},
	})
	if err != nil {
		t.Fatalf("OnAfterChat error: %v", err)
	}
	if len(store.items) != 1 {
		t.Fatalf("expected 1 fact stored, got %d", len(store.items))
	}
	for _, item := range store.items {
		if !strings.Contains(item.Memory, "dark mode") {
			t.Fatalf("expected stored fact to mention dark mode, got %q", item.Memory)
		}
	}
}

func TestOnAfterChatFallbackWithoutLLM(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}

	p := NewBuiltinProvider(slog.Default(), runtime, nil, nil)

	err := p.OnAfterChat(context.Background(), adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "Hello world"},
		},
	})
	if err != nil {
		t.Fatalf("OnAfterChat error: %v", err)
	}
	if len(store.items) != 1 {
		t.Fatalf("expected 1 item in store (legacy fallback), got %d", len(store.items))
	}
}

func TestOnBeforeChatRecallsFactMemory(t *testing.T) {
	t.Parallel()
	encoder := &fakeSparseEncoder{}
	index := newFakeSparseIndex(encoder)
	store := newFakeSparseStore()
	runtime := &sparseRuntime{qdrant: index, encoder: encoder, store: store}
	llm := &fakeLLM{
		extractFacts: []string{"User prefers oolong tea"},
		decideActions: []adapters.DecisionAction{
			{Event: "ADD", Text: "User prefers oolong tea"},
		},
	}

	p := NewBuiltinProvider(slog.Default(), runtime, nil, nil)
	p.SetLLM(llm)

	_ = p.OnAfterChat(context.Background(), adapters.AfterChatRequest{
		BotID: "bot-1",
		Messages: []adapters.Message{
			{Role: "user", Content: "I prefer oolong tea"},
		},
	})

	result, err := p.OnBeforeChat(context.Background(), adapters.BeforeChatRequest{
		BotID: "bot-1",
		Query: "tea",
	})
	if err != nil {
		t.Fatalf("OnBeforeChat error: %v", err)
	}
	if result == nil {
		t.Fatal("expected non-nil context result")
	}
	lower := strings.ToLower(result.ContextText)
	if !strings.Contains(lower, "oolong tea") {
		t.Fatalf("expected recalled context to mention oolong tea, got %q", result.ContextText)
	}
}
