package app

import (
	"context"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/nugget/thane-ai-agent/internal/channels/mqtt"
	"github.com/nugget/thane-ai-agent/internal/model/router"
	"github.com/nugget/thane-ai-agent/internal/platform/config"
	"github.com/nugget/thane-ai-agent/internal/platform/database"
	"github.com/nugget/thane-ai-agent/internal/platform/events"
	looppkg "github.com/nugget/thane-ai-agent/internal/runtime/loop"

	_ "github.com/mattn/go-sqlite3"
)

// mqttMockRunner records Run calls for assertion.
type mqttMockRunner struct {
	mu   sync.Mutex
	reqs []looppkg.Request
}

func (m *mqttMockRunner) Run(_ context.Context, req looppkg.Request, _ looppkg.StreamCallback) (*looppkg.Response, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.reqs = append(m.reqs, req)
	return &looppkg.Response{
		Content:       "ok",
		Model:         "test-model",
		InputTokens:   11,
		OutputTokens:  3,
		ContextWindow: 4096,
		RequestID:     "req-mqtt-test",
		ActiveTags:    append([]string(nil), req.InitialTags...),
	}, nil
}

func (m *mqttMockRunner) requests() []looppkg.Request {
	m.mu.Lock()
	defer m.mu.Unlock()
	cp := make([]looppkg.Request, len(m.reqs))
	copy(cp, m.reqs)
	return cp
}

func newTestWakeStore(t *testing.T) *mqtt.SubscriptionStore {
	t.Helper()
	db, err := database.OpenMemory()
	if err != nil {
		t.Fatalf("open memory db: %v", err)
	}
	t.Cleanup(func() { db.Close() })

	store, err := mqtt.NewSubscriptionStore(db, nil)
	if err != nil {
		t.Fatalf("new subscription store: %v", err)
	}
	return store
}

func TestMQTTWakeHandlerMatchingTopic(t *testing.T) {
	store := newTestWakeStore(t)
	seed := router.LoopProfile{
		Mission:          "automation",
		QualityFloor:     "7",
		DelegationGating: "disabled",
		Instructions:     "handle this",
	}
	if err := store.LoadConfig([]config.SubscriptionConfig{
		{Topic: "test/+/wake", Wake: &seed},
	}); err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	runner := &mqttMockRunner{}
	registry := looppkg.NewRegistry()
	bus := events.New()

	var parentID atomic.Value
	parentID.Store("test-mqtt-parent")

	deps := mqttWakeDeps{
		registry: registry,
		eventBus: bus,
		parentID: &parentID,
	}

	handler := mqttWakeHandler(store, runner, nil, nil, deps)
	handler("test/foo/wake", []byte(`{"event": "motion"}`))

	// Wait for goroutine dispatch.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if reqs := runner.requests(); len(reqs) > 0 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	reqs := runner.requests()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 request, got %d", len(reqs))
	}

	req := reqs[0]
	if req.Hints["source"] != "mqtt_wake" {
		t.Errorf("source hint = %q, want %q", req.Hints["source"], "mqtt_wake")
	}
	if req.Hints["mqtt_topic"] != "test/foo/wake" {
		t.Errorf("mqtt_topic hint = %q, want %q", req.Hints["mqtt_topic"], "test/foo/wake")
	}
	if req.Hints[router.HintMission] != "automation" {
		t.Errorf("mission hint = %q, want %q", req.Hints[router.HintMission], "automation")
	}
	if req.Hints[router.HintQualityFloor] != "7" {
		t.Errorf("quality_floor hint = %q, want %q", req.Hints[router.HintQualityFloor], "7")
	}

	// Verify message contains instructions and payload.
	msg := req.Messages[0].Content
	if msg == "" {
		t.Fatal("message content is empty")
	}
	if !contains(msg, "handle this") {
		t.Errorf("message should contain instructions, got %q", msg)
	}
	if !contains(msg, "motion") {
		t.Errorf("message should contain payload, got %q", msg)
	}
}

func TestMQTTWakeHandlerNoMatchFallback(t *testing.T) {
	store := newTestWakeStore(t)

	var fallbackCalled bool
	fallback := func(topic string, payload []byte) {
		fallbackCalled = true
	}

	runner := &mqttMockRunner{}
	handler := mqttWakeHandler(store, runner, fallback, nil, mqttWakeDeps{})

	handler("unmatched/topic", []byte("data"))

	if !fallbackCalled {
		t.Error("expected fallback to be called")
	}
	if reqs := runner.requests(); len(reqs) != 0 {
		t.Errorf("expected 0 requests, got %d", len(reqs))
	}
}

func TestMQTTWakeHandlerWithRegistry(t *testing.T) {
	store := newTestWakeStore(t)
	seed := router.LoopProfile{Instructions: "test instructions"}
	if err := store.LoadConfig([]config.SubscriptionConfig{
		{Topic: "home/sensor/wake", Wake: &seed},
	}); err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	runner := &mqttMockRunner{}
	registry := looppkg.NewRegistry()
	bus := events.New()

	var parentID atomic.Value
	parentID.Store("parent-loop-123")

	deps := mqttWakeDeps{
		registry: registry,
		eventBus: bus,
		parentID: &parentID,
	}

	handler := mqttWakeHandler(store, runner, nil, nil, deps)
	handler("home/sensor/wake", []byte(`{"temp": 22}`))

	// Wait for the spawned loop to complete.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if reqs := runner.requests(); len(reqs) > 0 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	reqs := runner.requests()
	if len(reqs) != 1 {
		t.Fatalf("expected 1 request via registry dispatch, got %d", len(reqs))
	}

	req := reqs[0]
	if req.Hints["source"] != "mqtt_wake" {
		t.Errorf("source hint = %q, want %q", req.Hints["source"], "mqtt_wake")
	}
	if req.Hints["loop_id"] == "" {
		t.Error("loop_id hint is empty; request did not traverse loop turn preparation")
	}
	if req.Hints["loop_name"] != "mqtt/wake" {
		t.Errorf("loop_name hint = %q, want %q", req.Hints["loop_name"], "mqtt/wake")
	}
}

func TestMQTTWakeHandlerFanOut(t *testing.T) {
	store := newTestWakeStore(t)
	seedA := router.LoopProfile{Mission: "automation", Instructions: "check temperature"}
	seedB := router.LoopProfile{Mission: "background", Instructions: "log reading"}
	if err := store.LoadConfig([]config.SubscriptionConfig{
		{Topic: "sensors/+/reading", Wake: &seedA},
		{Topic: "sensors/+/reading", Wake: &seedB},
	}); err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	runner := &mqttMockRunner{}
	registry := looppkg.NewRegistry()
	bus := events.New()

	var parentID atomic.Value
	parentID.Store("test-mqtt-parent")

	deps := mqttWakeDeps{
		registry: registry,
		eventBus: bus,
		parentID: &parentID,
	}

	handler := mqttWakeHandler(store, runner, nil, nil, deps)
	handler("sensors/temp/reading", []byte(`{"value": 22.5}`))

	// Wait for both goroutines to dispatch.
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if reqs := runner.requests(); len(reqs) >= 2 {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}

	reqs := runner.requests()
	if len(reqs) != 2 {
		t.Fatalf("expected 2 requests (fan-out), got %d", len(reqs))
	}

	// Both should have the subscription ID hint set.
	for i, req := range reqs {
		if req.Hints["mqtt_subscription_id"] == "" {
			t.Errorf("request %d missing mqtt_subscription_id hint", i)
		}
	}
}

func TestMQTTWakeHandlerNoRegistryDropsMessage(t *testing.T) {
	store := newTestWakeStore(t)
	seed := router.LoopProfile{Mission: "automation"}
	if err := store.LoadConfig([]config.SubscriptionConfig{
		{Topic: "test/topic", Wake: &seed},
	}); err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	runner := &mqttMockRunner{}
	// No registry — message should be dropped, not dispatched directly.
	handler := mqttWakeHandler(store, runner, nil, nil, mqttWakeDeps{})
	handler("test/topic", []byte("data"))

	// Give goroutine time to (not) run.
	time.Sleep(200 * time.Millisecond)

	if reqs := runner.requests(); len(reqs) != 0 {
		t.Fatalf("expected 0 requests without registry, got %d", len(reqs))
	}
}

func TestBuildWakeMessage(t *testing.T) {
	tests := []struct {
		name         string
		topic        string
		payload      []byte
		instructions string
		wantContains []string
	}{
		{
			name:         "no instructions",
			topic:        "test/topic",
			payload:      []byte("hello"),
			wantContains: []string{"test/topic", "hello"},
		},
		{
			name:         "with instructions",
			topic:        "test/topic",
			payload:      []byte("hello"),
			instructions: "do something",
			wantContains: []string{"Instructions: do something", "test/topic", "hello"},
		},
		{
			name:         "empty payload no instructions",
			topic:        "test/topic",
			payload:      nil,
			wantContains: []string{"test/topic"},
		},
		{
			name:         "empty payload with instructions",
			topic:        "test/topic",
			payload:      nil,
			instructions: "check status",
			wantContains: []string{"check status", "test/topic", "no payload"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			msg := buildWakeMessage(tt.topic, tt.payload, tt.instructions)
			for _, s := range tt.wantContains {
				if !contains(msg, s) {
					t.Errorf("message %q should contain %q", msg, s)
				}
			}
		})
	}
}

func TestApplyLoopProfile(t *testing.T) {
	seed := router.LoopProfile{
		Model:            "claude-3-opus",
		QualityFloor:     "8",
		Mission:          "automation",
		LocalOnly:        "false",
		DelegationGating: "disabled",
		ExcludeTools:     []string{"shell_exec"},
		ExtraHints:       map[string]string{"custom": "value"},
	}

	req := &looppkg.Request{
		Hints:        map[string]string{"existing": "hint"},
		ExcludeTools: []string{"files_read"},
		InitialTags:  []string{"baseline"},
	}

	applyLoopProfile(&seed, req)

	if req.Model != "claude-3-opus" {
		t.Errorf("Model = %q, want %q", req.Model, "claude-3-opus")
	}
	if req.Hints[router.HintQualityFloor] != "8" {
		t.Errorf("quality_floor = %q, want %q", req.Hints[router.HintQualityFloor], "8")
	}
	if req.Hints[router.HintMission] != "automation" {
		t.Errorf("mission = %q, want %q", req.Hints[router.HintMission], "automation")
	}
	if req.Hints["existing"] != "hint" {
		t.Errorf("existing hint was overwritten")
	}
	if req.Hints["custom"] != "value" {
		t.Errorf("extra hint not applied")
	}
	if len(req.ExcludeTools) != 2 || req.ExcludeTools[0] != "files_read" || req.ExcludeTools[1] != "shell_exec" {
		t.Errorf("ExcludeTools = %v, want [files_read shell_exec]", req.ExcludeTools)
	}
	// applyLoopProfile is purely routing/configuration; capability tags
	// are no longer part of LoopProfile and are applied separately by
	// the caller (e.g., from WakeSubscription.InitialTags).
	if len(req.InitialTags) != 1 || req.InitialTags[0] != "baseline" {
		t.Errorf("InitialTags = %v, want [baseline] (profile must not touch tags)", req.InitialTags)
	}
}

// contains is a test helper for string containment checks.
func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
		(len(substr) <= len(s) && searchString(s, substr)))
}

func searchString(s, substr string) bool {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return true
		}
	}
	return false
}
