package tools

import (
	"context"
	"encoding/json"
	"reflect"
	"sort"
	"strings"
	"testing"
	"time"

	"github.com/nugget/thane-ai-agent/internal/model/router"
	looppkg "github.com/nugget/thane-ai-agent/internal/runtime/loop"
)

type testLoopDefinitionDeps struct {
	reg              *Registry
	defs             *looppkg.DefinitionRegistry
	persisted        map[string]looppkg.Spec
	persistedUpdated map[string]time.Time
	deleted          []string
	persistedPolicy  map[string]looppkg.DefinitionPolicy
	deletedPolicy    []string
	reconciled       []string
	lastLaunch       looppkg.Launch
}

func newTestLoopDefinitionDeps(t *testing.T) *testLoopDefinitionDeps {
	t.Helper()

	defs, err := looppkg.NewDefinitionRegistry([]looppkg.Spec{
		{
			Name:       "metacog_like",
			Enabled:    true,
			Task:       "Observe and reflect.",
			Operation:  looppkg.OperationService,
			Completion: looppkg.CompletionNone,
			Profile: router.LoopProfile{
				Mission: "background",
			},
		},
	})
	if err != nil {
		t.Fatalf("NewDefinitionRegistry: %v", err)
	}

	reg := NewEmptyRegistry()
	deps := &testLoopDefinitionDeps{
		reg:              reg,
		defs:             defs,
		persisted:        make(map[string]looppkg.Spec),
		persistedUpdated: make(map[string]time.Time),
		persistedPolicy:  make(map[string]looppkg.DefinitionPolicy),
	}
	reg.ConfigureLoopDefinitionTools(LoopDefinitionToolDeps{
		Registry: defs,
		View: func() *looppkg.DefinitionRegistryView {
			return looppkg.BuildDefinitionRegistryView(defs.Snapshot(), map[string]looppkg.DefinitionRuntimeStatus{
				"metacog_like": {
					Running: true,
					LoopID:  "loop-live-1",
					State:   looppkg.StateSleeping,
				},
			})
		},
		PersistSpec: func(spec looppkg.Spec, updatedAt time.Time) error {
			deps.persisted[spec.Name] = spec
			deps.persistedUpdated[spec.Name] = updatedAt
			return nil
		},
		DeleteSpec: func(name string) error {
			deps.deleted = append(deps.deleted, name)
			delete(deps.persisted, name)
			delete(deps.persistedUpdated, name)
			return nil
		},
		PersistPolicy: func(name string, policy looppkg.DefinitionPolicy) error {
			deps.persistedPolicy[name] = policy
			return nil
		},
		DeletePolicy: func(name string) error {
			deps.deletedPolicy = append(deps.deletedPolicy, name)
			delete(deps.persistedPolicy, name)
			return nil
		},
		Reconcile: func(_ context.Context, name string) error {
			deps.reconciled = append(deps.reconciled, name)
			return nil
		},
		LaunchDefinition: func(_ context.Context, name string, launch looppkg.Launch) (looppkg.LaunchResult, error) {
			deps.lastLaunch = launch
			spec, ok := deps.defs.Get(name)
			if !ok {
				return looppkg.LaunchResult{}, &looppkg.UnknownDefinitionError{Name: name}
			}
			return looppkg.LaunchResult{
				LoopID:    "loop-123",
				Operation: spec.Operation,
				Detached:  spec.Operation != looppkg.OperationRequestReply,
			}, nil
		},
	})
	return deps
}

func sharedLoopLaunchSchemaKeys() []string {
	typ := reflect.TypeOf(looppkg.Launch{})
	keys := make([]string, 0, typ.NumField())
	for i := 0; i < typ.NumField(); i++ {
		field := typ.Field(i)
		name, _, _ := strings.Cut(field.Tag.Get("json"), ",")
		switch name {
		case "", "-", "spec":
			continue
		default:
			keys = append(keys, name)
		}
	}
	sort.Strings(keys)
	return keys
}

func schemaProperties(t *testing.T, schema map[string]any) map[string]any {
	t.Helper()

	props, ok := schema["properties"].(map[string]any)
	if !ok {
		t.Fatalf("schema properties = %#v, want map[string]any", schema["properties"])
	}
	return props
}

func schemaObjectProperty(t *testing.T, schema map[string]any, key string) map[string]any {
	t.Helper()

	props := schemaProperties(t, schema)
	raw, ok := props[key]
	if !ok {
		t.Fatalf("schema missing %q property", key)
	}
	child, ok := raw.(map[string]any)
	if !ok {
		t.Fatalf("schema property %q = %#v, want map[string]any", key, raw)
	}
	return child
}

func TestConfigureLoopDefinitionTools_RegistersTools(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	for _, name := range []string{
		"loop_definition_summary",
		"loop_definition_list",
		"loop_definition_get",
		"loop_definition_lint",
		"loop_definition_set",
		"loop_definition_delete",
		"loop_definition_set_policy",
		"loop_definition_launch",
	} {
		if deps.reg.Get(name) == nil {
			t.Fatalf("%s tool not registered", name)
		}
	}
	if deps.reg.loopDefinitionRegistry != deps.defs {
		t.Fatal("loop definition registry dependency was not stored")
	}
}

func TestLoopDefinitionGetIncludesWarnings(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	out, err := deps.reg.Get("loop_definition_get").Handler(context.Background(), map[string]any{
		"name": "metacog_like",
	})
	if err != nil {
		t.Fatalf("loop_definition_get: %v", err)
	}

	var got struct {
		Definition looppkg.DefinitionView `json:"definition"`
	}
	if err := json.Unmarshal([]byte(out), &got); err != nil {
		t.Fatalf("unmarshal get response: %v", err)
	}
	if len(got.Definition.Warnings) == 0 {
		t.Fatal("expected authoring warnings on metacog_like definition")
	}
	if got.Definition.Warnings[0].Code != "service_default_cadence" {
		t.Fatalf("warning code = %q, want service_default_cadence", got.Definition.Warnings[0].Code)
	}
}

func TestLoopDefinitionLintReportsServiceWarnings(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	out, err := deps.reg.Get("loop_definition_lint").Handler(context.Background(), map[string]any{
		"spec": map[string]any{
			"name":      "comal_burn_ban_monitor",
			"task":      "Check the county burn ban webpage hourly and update Home Assistant.",
			"operation": "service",
			"tags":      []any{"web", "ha"},
		},
	})
	if err != nil {
		t.Fatalf("loop_definition_lint: %v", err)
	}

	var got struct {
		Status          string                      `json:"status"`
		Valid           bool                        `json:"valid"`
		Warnings        []looppkg.DefinitionWarning `json:"warnings"`
		DefaultedFields []string                    `json:"defaulted_fields"`
		Effective       struct {
			Operation        looppkg.Operation `json:"operation"`
			SleepMin         string            `json:"sleep_min"`
			SleepMax         string            `json:"sleep_max"`
			SleepDefault     string            `json:"sleep_default"`
			Jitter           float64           `json:"jitter"`
			DelegationGating string            `json:"delegation_gating"`
		} `json:"effective"`
	}
	if err := json.Unmarshal([]byte(out), &got); err != nil {
		t.Fatalf("unmarshal lint response: %v", err)
	}
	if got.Status != "ok" || !got.Valid {
		t.Fatalf("lint response = %#v, want status ok and valid true", got)
	}
	if got.Effective.Operation != looppkg.OperationService {
		t.Fatalf("operation = %q, want service", got.Effective.Operation)
	}
	if got.Effective.SleepMin != "30s" || got.Effective.SleepMax != "5m0s" || got.Effective.SleepDefault != "1m0s" {
		t.Fatalf("effective sleep = %#v, want 30s/5m/1m", got.Effective)
	}
	if got.Effective.Jitter != looppkg.DefaultJitter {
		t.Fatalf("effective jitter = %v, want %v", got.Effective.Jitter, looppkg.DefaultJitter)
	}
	if len(got.Warnings) < 3 {
		t.Fatalf("warnings = %#v, want at least cadence/task/delegation warnings", got.Warnings)
	}
	codes := make(map[string]bool, len(got.Warnings))
	for _, warning := range got.Warnings {
		codes[warning.Code] = true
	}
	for _, code := range []string{
		"service_default_cadence",
		"task_mentions_cadence_without_explicit_sleep",
		"service_delegation_gating_enabled",
	} {
		if !codes[code] {
			t.Fatalf("warning codes = %#v, want %q", codes, code)
		}
	}
	if len(got.DefaultedFields) != 4 {
		t.Fatalf("defaulted_fields = %#v, want sleep_min/sleep_max/sleep_default/jitter", got.DefaultedFields)
	}
}

func TestLoopDefinitionSetAndDelete(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	out, err := deps.reg.Get("loop_definition_set").Handler(context.Background(), map[string]any{
		"spec": map[string]any{
			"name":       "room_monitor",
			"task":       "Monitor the office and report durable changes.",
			"operation":  "service",
			"completion": "conversation",
			"profile": map[string]any{
				"mission":      "background",
				"initial_tags": []any{"homeassistant"},
			},
			"sleep_min":     "5m",
			"sleep_max":     "30m",
			"sleep_default": "10m",
		},
	})
	if err != nil {
		t.Fatalf("loop_definition_set: %v", err)
	}

	var setResp struct {
		Status     string                 `json:"status"`
		Generation int64                  `json:"generation"`
		Definition looppkg.DefinitionView `json:"definition"`
	}
	if err := json.Unmarshal([]byte(out), &setResp); err != nil {
		t.Fatalf("unmarshal set response: %v", err)
	}
	if setResp.Status != "ok" {
		t.Fatalf("status = %q, want ok", setResp.Status)
	}
	if setResp.Definition.Source != looppkg.DefinitionSourceOverlay {
		t.Fatalf("source = %q, want overlay", setResp.Definition.Source)
	}
	if _, ok := deps.persisted["room_monitor"]; !ok {
		t.Fatal("persist callback was not invoked")
	}

	deleteOut, err := deps.reg.Get("loop_definition_delete").Handler(context.Background(), map[string]any{
		"name": "room_monitor",
	})
	if err != nil {
		t.Fatalf("loop_definition_delete: %v", err)
	}
	var deleteResp struct {
		Status     string `json:"status"`
		Generation int64  `json:"generation"`
		Name       string `json:"name"`
	}
	if err := json.Unmarshal([]byte(deleteOut), &deleteResp); err != nil {
		t.Fatalf("unmarshal delete response: %v", err)
	}
	if deleteResp.Name != "room_monitor" {
		t.Fatalf("name = %q, want room_monitor", deleteResp.Name)
	}
	if len(deps.deleted) != 1 || deps.deleted[0] != "room_monitor" {
		t.Fatalf("deleted = %v, want [room_monitor]", deps.deleted)
	}
	if len(deps.reconciled) != 2 || deps.reconciled[0] != "room_monitor" || deps.reconciled[1] != "room_monitor" {
		t.Fatalf("reconciled = %v, want [room_monitor room_monitor]", deps.reconciled)
	}
}

func TestLoopDefinitionListFiltersOverlay(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if err := deps.defs.Upsert(looppkg.Spec{
		Name:       "room_monitor",
		Task:       "Monitor the office.",
		Operation:  looppkg.OperationService,
		Completion: looppkg.CompletionConversation,
		Profile: router.LoopProfile{
			Mission: "background",
		},
	}, time.Now()); err != nil {
		t.Fatalf("Upsert: %v", err)
	}

	out, err := deps.reg.Get("loop_definition_list").Handler(context.Background(), map[string]any{
		"source": "overlay",
	})
	if err != nil {
		t.Fatalf("loop_definition_list: %v", err)
	}

	var got struct {
		Count int                      `json:"count"`
		Items []looppkg.DefinitionView `json:"items"`
	}
	if err := json.Unmarshal([]byte(out), &got); err != nil {
		t.Fatalf("unmarshal list: %v", err)
	}
	if got.Count != 1 {
		t.Fatalf("count = %d, want 1", got.Count)
	}
	if got.Items[0].Name != "room_monitor" {
		t.Fatalf("item name = %q, want room_monitor", got.Items[0].Name)
	}
}

func TestLoopDefinitionSetPolicyAndLaunch(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	out, err := deps.reg.Get("loop_definition_set_policy").Handler(context.Background(), map[string]any{
		"name":   "metacog_like",
		"state":  "inactive",
		"reason": "quiet hours",
	})
	if err != nil {
		t.Fatalf("loop_definition_set_policy: %v", err)
	}

	var policyResp struct {
		Status     string                 `json:"status"`
		Generation int64                  `json:"generation"`
		Definition looppkg.DefinitionView `json:"definition"`
	}
	if err := json.Unmarshal([]byte(out), &policyResp); err != nil {
		t.Fatalf("unmarshal policy response: %v", err)
	}
	if policyResp.Definition.PolicyState != looppkg.DefinitionPolicyStateInactive || policyResp.Definition.PolicySource != looppkg.DefinitionPolicySourceOverlay {
		t.Fatalf("policy = %q/%q, want inactive/overlay", policyResp.Definition.PolicyState, policyResp.Definition.PolicySource)
	}
	if deps.persistedPolicy["metacog_like"].Reason != "quiet hours" {
		t.Fatalf("persisted policy = %+v, want quiet hours", deps.persistedPolicy["metacog_like"])
	}
	if policyResp.Definition.Runtime.LoopID != "loop-live-1" {
		t.Fatalf("runtime loop_id = %q, want loop-live-1", policyResp.Definition.Runtime.LoopID)
	}

	launchOut, err := deps.reg.Get("loop_definition_launch").Handler(context.Background(), map[string]any{
		"name": "metacog_like",
	})
	if err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}
	var launchResp struct {
		Status string               `json:"status"`
		Result looppkg.LaunchResult `json:"result"`
	}
	if err := json.Unmarshal([]byte(launchOut), &launchResp); err != nil {
		t.Fatalf("unmarshal launch response: %v", err)
	}
	if launchResp.Result.LoopID != "loop-123" {
		t.Fatalf("launch loop_id = %q, want loop-123", launchResp.Result.LoopID)
	}
}

func TestLoopDefinitionLaunchRoutesTopLevelModelOverride(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if _, err := deps.reg.Get("loop_definition_launch").Handler(context.Background(), map[string]any{
		"name": "metacog_like",
		"launch": map[string]any{
			"model": "claude-sonnet-4-5",
		},
	}); err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}
	if deps.lastLaunch.Model != "claude-sonnet-4-5" {
		t.Fatalf("lastLaunch.Model = %q, want claude-sonnet-4-5", deps.lastLaunch.Model)
	}
}

func TestLoopDefinitionLaunchSchemaExposesSharedLaunchFields(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	tool := deps.reg.Get("loop_definition_launch")
	if tool == nil {
		t.Fatal("loop_definition_launch tool not registered")
	}

	launchSchema := schemaObjectProperty(t, tool.Parameters, "launch")
	launchProps := schemaProperties(t, launchSchema)

	for _, key := range sharedLoopLaunchSchemaKeys() {
		if _, ok := launchProps[key]; !ok {
			t.Errorf("launch schema missing %q", key)
		}
	}

	if got := schemaObjectProperty(t, launchSchema, "run_timeout")["type"]; got != "string" {
		t.Fatalf("run_timeout.type = %#v, want string", got)
	}
	if got := schemaObjectProperty(t, launchSchema, "tool_timeout")["type"]; got != "string" {
		t.Fatalf("tool_timeout.type = %#v, want string", got)
	}

	completionProps := schemaProperties(t, schemaObjectProperty(t, launchSchema, "completion_channel"))
	for _, key := range []string{"channel", "recipient", "conversation_id"} {
		if _, ok := completionProps[key]; !ok {
			t.Errorf("completion_channel schema missing %q", key)
		}
	}

	channelBindingProps := schemaProperties(t, schemaObjectProperty(t, launchSchema, "channel_binding"))
	for _, key := range []string{"channel", "address", "contact_id", "contact_name", "trust_zone", "link_source", "is_owner"} {
		if _, ok := channelBindingProps[key]; !ok {
			t.Errorf("channel_binding schema missing %q", key)
		}
	}
}

func TestLoopDefinitionLaunchRejectsModelInsideMetadata(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	_, err := deps.reg.Get("loop_definition_launch").Handler(context.Background(), map[string]any{
		"name": "metacog_like",
		"launch": map[string]any{
			"metadata": map[string]any{
				"model": "claude-sonnet-4-5",
			},
		},
	})
	if err == nil {
		t.Fatal("expected error for metadata.model override, got nil")
	}
	if !strings.Contains(err.Error(), "metadata.model") || !strings.Contains(err.Error(), "launch.model") {
		t.Fatalf("error = %q, want guidance pointing from metadata.model to launch.model", err.Error())
	}
	if deps.lastLaunch.Model != "" {
		t.Fatalf("lastLaunch.Model = %q, want empty (launch should not have been dispatched)", deps.lastLaunch.Model)
	}
}

func TestLoopDefinitionLaunchDefaultsCompletionTargetFromSignalContext(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if err := deps.defs.Upsert(looppkg.Spec{
		Name:       "signal_background",
		Task:       "Report back later.",
		Operation:  looppkg.OperationBackgroundTask,
		Completion: looppkg.CompletionChannel,
	}, time.Now()); err != nil {
		t.Fatalf("Upsert: %v", err)
	}

	ctx := WithConversationID(context.Background(), "signal-15551234567")
	ctx = WithHints(ctx, map[string]string{
		"source": "signal",
		"sender": "+15551234567",
	})

	if _, err := deps.reg.Get("loop_definition_launch").Handler(ctx, map[string]any{
		"name": "signal_background",
	}); err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}

	if deps.lastLaunch.CompletionChannel == nil {
		t.Fatal("CompletionChannel = nil, want inferred signal target")
	}
	if deps.lastLaunch.CompletionChannel.Channel != "signal" || deps.lastLaunch.CompletionChannel.Recipient != "+15551234567" || deps.lastLaunch.CompletionChannel.ConversationID != "signal-15551234567" {
		t.Fatalf("CompletionChannel = %#v", deps.lastLaunch.CompletionChannel)
	}
}

func TestLoopDefinitionLaunchDefaultsConversationCompletionFromSignalContext(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if err := deps.defs.Upsert(looppkg.Spec{
		Name:       "signal_conversation",
		Task:       "Keep me posted here.",
		Operation:  looppkg.OperationBackgroundTask,
		Completion: looppkg.CompletionConversation,
	}, time.Now()); err != nil {
		t.Fatalf("Upsert: %v", err)
	}

	ctx := WithConversationID(context.Background(), "signal-15551234567")
	ctx = WithHints(ctx, map[string]string{
		"source": "signal",
		"sender": "+15551234567",
	})

	if _, err := deps.reg.Get("loop_definition_launch").Handler(ctx, map[string]any{
		"name": "signal_conversation",
	}); err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}

	if deps.lastLaunch.CompletionConversationID != "signal-15551234567" {
		t.Fatalf("CompletionConversationID = %q, want signal-15551234567", deps.lastLaunch.CompletionConversationID)
	}
}

func TestLoopDefinitionLaunchDefaultsCompletionTargetFromOWUContext(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if err := deps.defs.Upsert(looppkg.Spec{
		Name:       "owu_background",
		Task:       "Report back later.",
		Operation:  looppkg.OperationBackgroundTask,
		Completion: looppkg.CompletionChannel,
	}, time.Now()); err != nil {
		t.Fatalf("Upsert: %v", err)
	}

	ctx := WithConversationID(context.Background(), "owu-abc123")

	if _, err := deps.reg.Get("loop_definition_launch").Handler(ctx, map[string]any{
		"name": "owu_background",
	}); err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}

	if deps.lastLaunch.CompletionChannel == nil {
		t.Fatal("CompletionChannel = nil, want inferred owu target")
	}
	if deps.lastLaunch.CompletionChannel.Channel != "owu" || deps.lastLaunch.CompletionChannel.ConversationID != "owu-abc123" {
		t.Fatalf("CompletionChannel = %#v", deps.lastLaunch.CompletionChannel)
	}
}

func TestLoopDefinitionLaunchDefaultsConversationCompletionFromOWUContext(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	if err := deps.defs.Upsert(looppkg.Spec{
		Name:       "owu_conversation",
		Task:       "Keep me posted here.",
		Operation:  looppkg.OperationBackgroundTask,
		Completion: looppkg.CompletionConversation,
	}, time.Now()); err != nil {
		t.Fatalf("Upsert: %v", err)
	}

	ctx := WithConversationID(context.Background(), "owu-abc123")

	if _, err := deps.reg.Get("loop_definition_launch").Handler(ctx, map[string]any{
		"name": "owu_conversation",
	}); err != nil {
		t.Fatalf("loop_definition_launch: %v", err)
	}

	if deps.lastLaunch.CompletionConversationID != "owu-abc123" {
		t.Fatalf("CompletionConversationID = %q, want owu-abc123", deps.lastLaunch.CompletionConversationID)
	}
}

func TestLoopDefinitionListFiltersRuntimeState(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	out, err := deps.reg.Get("loop_definition_list").Handler(context.Background(), map[string]any{
		"runtime_state": "sleeping",
	})
	if err != nil {
		t.Fatalf("loop_definition_list(runtime_state): %v", err)
	}

	var got struct {
		Count int                      `json:"count"`
		Items []looppkg.DefinitionView `json:"items"`
	}
	if err := json.Unmarshal([]byte(out), &got); err != nil {
		t.Fatalf("unmarshal list: %v", err)
	}
	if got.Count != 1 || got.Items[0].Name != "metacog_like" {
		t.Fatalf("items = %+v, want one sleeping metacog_like", got.Items)
	}
}

func TestLoopDefinitionListFiltersEligibility(t *testing.T) {
	deps := newTestLoopDefinitionDeps(t)

	deps.reg.loopDefinitionView = func() *looppkg.DefinitionRegistryView {
		return &looppkg.DefinitionRegistryView{
			Generation: 1,
			Definitions: []looppkg.DefinitionView{
				{
					DefinitionSnapshot: looppkg.DefinitionSnapshot{
						Name: "eligible_watch",
						Spec: looppkg.Spec{
							Name:      "eligible_watch",
							Task:      "watch",
							Operation: looppkg.OperationService,
						},
					},
					Eligibility: looppkg.DefinitionEligibilityStatus{Eligible: true},
				},
				{
					DefinitionSnapshot: looppkg.DefinitionSnapshot{
						Name: "ineligible_watch",
						Spec: looppkg.Spec{
							Name:      "ineligible_watch",
							Task:      "watch later",
							Operation: looppkg.OperationService,
						},
					},
					Eligibility: looppkg.DefinitionEligibilityStatus{
						Eligible: false,
						Reason:   "outside scheduled windows",
					},
				},
			},
		}
	}

	out, err := deps.reg.Get("loop_definition_list").Handler(context.Background(), map[string]any{
		"eligible": "false",
	})
	if err != nil {
		t.Fatalf("loop_definition_list(eligible): %v", err)
	}

	var got struct {
		Count int                      `json:"count"`
		Items []looppkg.DefinitionView `json:"items"`
	}
	if err := json.Unmarshal([]byte(out), &got); err != nil {
		t.Fatalf("unmarshal list: %v", err)
	}
	if got.Count != 1 || got.Items[0].Name != "ineligible_watch" {
		t.Fatalf("items = %+v, want one ineligible_watch definition", got.Items)
	}
}
