package configrefresh_test

import (
	"strings"
	"testing"

	"iop/apps/edge/internal/configrefresh"
	"iop/packages/go/config"
)

func TestClassifyExecutionPresetLiveApply(t *testing.T) {
	current := &config.EdgeConfig{
		ExecutionPresets: []config.ExecutionPreset{
			{
				ID:           "preset-z-remove",
				Selector:     config.ExecutionModelBinding{Model: "gpt-4o"},
				AllowedModes: []string{config.ModeDirect},
				Routes: map[string]config.ExecutionRoute{
					config.ModeDirect: {Stages: []config.ExecutionRouteStage{}},
				},
			},
			{
				ID:           "preset-m-mod",
				Selector:     config.ExecutionModelBinding{Model: "gpt-4o", Options: map[string]any{"a": 1}},
				AllowedModes: []string{config.ModeDirect},
				Routes: map[string]config.ExecutionRoute{
					config.ModeDirect: {Stages: []config.ExecutionRouteStage{}},
				},
				SingleRequest: &config.ExecutionSingleRequestPolicy{
					WorkspaceRef: "ws-ref-current",
					Limits: config.ExecutionSingleRequestLimits{
						WallClockMS:       10 * 60 * 1000,
						StageTimeoutMS:    5 * 60 * 1000,
						MaxToolIterations: 32,
						MaxOutputBytes:    8 * 1024 * 1024,
					},
					Stages: config.ExecutionSingleRequestStages{
						Plan:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						Work:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o-mini"},
						Review: config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
					},
				},
			},
		},
	}

	// Candidate list has IDs out of lexical order (preset-a-add first, then preset-m-mod)
	candidate := &config.EdgeConfig{
		ExecutionPresets: []config.ExecutionPreset{
			{
				ID:           "preset-a-add",
				Selector:     config.ExecutionModelBinding{Model: "claude-3-5-sonnet"},
				AllowedModes: []string{config.ModeDirect},
				Routes: map[string]config.ExecutionRoute{
					config.ModeDirect: {Stages: []config.ExecutionRouteStage{}},
				},
			},
			{
				ID:           "preset-m-mod",
				Selector:     config.ExecutionModelBinding{Model: "gpt-4o-mini", Options: map[string]any{"a": 2}},
				AllowedModes: []string{config.ModeDirect, config.ModeLight},
				Routes: map[string]config.ExecutionRoute{
					config.ModeDirect: {Stages: []config.ExecutionRouteStage{}},
					config.ModeLight: {
						Stages: []config.ExecutionRouteStage{
							{Role: "local", Model: "gpt-4o"},
							{Role: "review", Model: "gpt-4o"},
						},
					},
				},
				SingleRequest: &config.ExecutionSingleRequestPolicy{
					WorkspaceRef: "ws-ref-next",
					Limits: config.ExecutionSingleRequestLimits{
						WallClockMS:       20 * 60 * 1000,
						StageTimeoutMS:    8 * 60 * 1000,
						MaxToolIterations: 64,
						MaxOutputBytes:    16 * 1024 * 1024,
					},
					Stages: config.ExecutionSingleRequestStages{
						Plan:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						Work:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o-mini"},
						Review: config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
					},
				},
				WorkspaceTools: []config.ExecutionWorkspaceToolAlternative{
					{
						Name: "default",
						Operations: map[string]config.ExecutionWorkspaceOperation{
							"read":   {ToolName: "file_read", SchemaMatcher: map[string]any{"sm": "r"}, ArgumentMap: map[string]any{"arg": "r"}, ResultMatcher: map[string]any{"res": "r"}},
							"write":  {ToolName: "file_write", CreatesParents: true, SchemaMatcher: map[string]any{"sm": "w"}, ArgumentMap: map[string]any{"arg": "w"}, ResultMatcher: map[string]any{"res": "w"}},
							"delete": {ToolName: "file_delete", SchemaMatcher: map[string]any{"sm": "d"}, ArgumentMap: map[string]any{"arg": "d"}, ResultMatcher: map[string]any{"res": "d"}},
						},
					},
				},
			},
		},
	}

	result := configrefresh.Classify(current, candidate)
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=%q, got %q (changes: %+v)", configrefresh.StatusApplied, result.Status, result.Changes)
	}

	type expectedChange struct {
		path  string
		class configrefresh.Status
	}

	want := []expectedChange{
		{path: `execution_presets["preset-a-add"]`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-m-mod"].allowed_modes`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-m-mod"].routes`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-m-mod"].selector`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-m-mod"].single_request`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-m-mod"].workspace_tools`, class: configrefresh.StatusApplied},
		{path: `execution_presets["preset-z-remove"]`, class: configrefresh.StatusApplied},
	}

	if len(result.Changes) != len(want) {
		t.Fatalf("got %d changes, want %d (actual changes: %+v)", len(result.Changes), len(want), result.Changes)
	}

	for i, c := range result.Changes {
		if c.Path != want[i].path {
			t.Errorf("change[%d] path: got %q, want %q", i, c.Path, want[i].path)
		}
		if c.Class != want[i].class {
			t.Errorf("change[%d] class for %s: got %q, want %q", i, c.Path, c.Class, want[i].class)
		}
		if c.Path == `execution_presets["preset-m-mod"].single_request` {
			if !strings.Contains(c.Previous, "PlanDigest:") || !strings.Contains(c.Previous, "ReviewDigest:") {
				t.Errorf("single_request previous = %q, expected redacted template digests", c.Previous)
			}
			if !strings.Contains(c.Next, "PlanDigest:") || !strings.Contains(c.Next, "ReviewDigest:") {
				t.Errorf("single_request next = %q, expected redacted template digests", c.Next)
			}
		}
	}

	paths := make([]string, 0, len(result.Changes))
	for _, c := range result.Changes {
		paths = append(paths, c.Path)
	}
	routesIdx, srIdx, wsIdx := -1, -1, -1
	for i, p := range paths {
		switch {
		case p == `execution_presets["preset-m-mod"].routes`:
			routesIdx = i
		case p == `execution_presets["preset-m-mod"].single_request`:
			srIdx = i
		case p == `execution_presets["preset-m-mod"].workspace_tools`:
			wsIdx = i
		}
	}
	if routesIdx < 0 {
		t.Fatalf("expected a change at execution_presets[\"preset-m-mod\"].routes, got changes: %+v", result.Changes)
	}
	if srIdx < 0 {
		t.Fatalf("expected a change at execution_presets[\"preset-m-mod\"].single_request, got changes: %+v", result.Changes)
	}
	if wsIdx < 0 {
		t.Fatalf("expected a change at execution_presets[\"preset-m-mod\"].workspace_tools, got changes: %+v", result.Changes)
	}
	if routesIdx >= srIdx {
		t.Errorf("single_request path must appear after routes: routes@%d, single_request@%d", routesIdx, srIdx)
	}
	if srIdx >= wsIdx {
		t.Errorf("single_request path must appear before workspace_tools: single_request@%d, workspace_tools@%d", srIdx, wsIdx)
	}
}

// TestClassifyModelExecutionPresetLiveApply verifies that changing a virtual
// model's execution_preset mapping is reported as a single live-applied change
// and attributes the model in ChangedModels.
func TestClassifyModelExecutionPresetLiveApply(t *testing.T) {
	current := &config.EdgeConfig{
		Models: []config.ModelCatalogEntry{
			{ID: "virtual-model", ExecutionPreset: "preset-a"},
		},
	}
	candidate := &config.EdgeConfig{
		Models: []config.ModelCatalogEntry{
			{ID: "virtual-model", ExecutionPreset: "preset-b"},
		},
	}

	result := configrefresh.Classify(current, candidate)

	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=%q, got %q (changes: %+v)", configrefresh.StatusApplied, result.Status, result.Changes)
	}
	if result.Summary != "all changes can be applied without restart" {
		t.Errorf("summary = %q, want all-applied summary", result.Summary)
	}

	if len(result.Changes) != 1 {
		t.Fatalf("got %d changes, want 1 (actual: %+v)", len(result.Changes), result.Changes)
	}
	change := result.Changes[0]
	if change.Path != `models["virtual-model"].execution_preset` {
		t.Errorf("change path: got %q, want %q", change.Path, `models["virtual-model"].execution_preset`)
	}
	if change.Class != configrefresh.StatusApplied {
		t.Errorf("change class: got %q, want %q", change.Class, configrefresh.StatusApplied)
	}
	if change.Previous != "preset-a" {
		t.Errorf("change previous: got %q, want %q", change.Previous, "preset-a")
	}
	if change.Next != "preset-b" {
		t.Errorf("change next: got %q, want %q", change.Next, "preset-b")
	}

	if len(result.ChangedModels) != 1 || result.ChangedModels[0] != "virtual-model" {
		t.Errorf("ChangedModels = %v, want [virtual-model]", result.ChangedModels)
	}
}

// templatePath builds an explicitly configured optional template path. A nil
// field means the operator omitted the file, so configured fixtures must carry
// a pointer.
func templatePath(path string) *string {
	return &path
}

func TestClassifySingleRequestTemplateContentChange(t *testing.T) {
	current := &config.EdgeConfig{
		ExecutionPresets: []config.ExecutionPreset{
			{
				ID:           "preset-templates",
				Selector:     config.ExecutionModelBinding{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
				AllowedModes: []string{config.ModeLight},
				Routes: map[string]config.ExecutionRoute{
					config.ModeLight: {
						Stages: []config.ExecutionRouteStage{
							{Role: "plan", Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
							{Role: "work", Model: "gpt-4o-mini"},
							{Role: "review", Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						},
					},
				},
				SingleRequest: &config.ExecutionSingleRequestPolicy{
					WorkspaceRef: "ws-1",
					Limits: config.ExecutionSingleRequestLimits{
						WallClockMS:       60000,
						StageTimeoutMS:    30000,
						MaxToolIterations: 10,
						MaxOutputBytes:    1024,
					},
					Stages: config.ExecutionSingleRequestStages{
						Plan:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						Work:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o-mini"},
						Review: config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
					},
					Templates: config.ExecutionSingleRequestTemplates{
						PlanFile:        templatePath("templates/plan.md"),
						ReviewFile:      templatePath("templates/review.md"),
						EffectivePlan:   "Plan template v1\n# Plan\n## Goal\n{{goal}}\n## Steps\n{{steps}}\n## Verification\n{{verification}}",
						EffectiveReview: "Review template v1\n# Review\n## Result\nPASS\n## Checks\n{{checks}}\n## Verification\n{{verification}}\n## Summary\n{{summary}}",
					},
				},
			},
		},
	}

	candidate := &config.EdgeConfig{
		ExecutionPresets: []config.ExecutionPreset{
			{
				ID:           "preset-templates",
				Selector:     config.ExecutionModelBinding{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
				AllowedModes: []string{config.ModeLight},
				Routes: map[string]config.ExecutionRoute{
					config.ModeLight: {
						Stages: []config.ExecutionRouteStage{
							{Role: "plan", Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
							{Role: "work", Model: "gpt-4o-mini"},
							{Role: "review", Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						},
					},
				},
				SingleRequest: &config.ExecutionSingleRequestPolicy{
					WorkspaceRef: "ws-1",
					Limits: config.ExecutionSingleRequestLimits{
						WallClockMS:       60000,
						StageTimeoutMS:    30000,
						MaxToolIterations: 10,
						MaxOutputBytes:    1024,
					},
					Stages: config.ExecutionSingleRequestStages{
						Plan:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
						Work:   config.ExecutionSingleRequestStageConfig{Model: "gpt-4o-mini"},
						Review: config.ExecutionSingleRequestStageConfig{Model: "gpt-4o", Options: map[string]any{"reasoning_effort": "high"}},
					},
					Templates: config.ExecutionSingleRequestTemplates{
						PlanFile:        templatePath("templates/plan.md"), // same path, distinct pointer
						ReviewFile:      templatePath("templates/review.md"),
						EffectivePlan:   "Plan template v2\n# Plan\n## Goal\n{{goal}}\n## Steps\n{{steps}}\n## Verification\n{{verification}}", // content changed
						EffectiveReview: "Review template v1\n# Review\n## Result\nPASS\n## Checks\n{{checks}}\n## Verification\n{{verification}}\n## Summary\n{{summary}}",
					},
				},
			},
		},
	}

	result := configrefresh.Classify(current, candidate)
	if result.Status != configrefresh.StatusApplied {
		t.Fatalf("expected status=%q, got %q (changes: %+v)", configrefresh.StatusApplied, result.Status, result.Changes)
	}
	if len(result.Changes) != 1 {
		t.Fatalf("expected 1 change, got %d", len(result.Changes))
	}

	c := result.Changes[0]
	if c.Path != `execution_presets["preset-templates"].single_request` {
		t.Errorf("change path = %q, want execution_presets[\"preset-templates\"].single_request", c.Path)
	}
	if c.Class != configrefresh.StatusApplied {
		t.Errorf("change class = %q, want StatusApplied", c.Class)
	}
	if strings.Contains(c.Previous, "templates/plan.md") || strings.Contains(c.Next, "templates/plan.md") {
		t.Errorf("change output exposes file path: prev=%q, next=%q", c.Previous, c.Next)
	}
	if strings.Contains(c.Previous, "Plan template v1") || strings.Contains(c.Next, "Plan template v2") {
		t.Errorf("change output exposes raw template body: prev=%q, next=%q", c.Previous, c.Next)
	}
}
