package service

import (
	"errors"
	"strings"
	"testing"

	"iop/packages/go/config"
	"iop/packages/go/singlerequesttemplate"
)

func validLimits() SingleRequestLimits {
	return SingleRequestLimits{
		WallClockMS:       30 * 60 * 1000,
		StageTimeoutMS:    10 * 60 * 1000,
		MaxToolIterations: 64,
		MaxOutputBytes:    16 * 1024 * 1024,
	}
}

func validStages() (SingleRequestStageBinding, SingleRequestStageBinding, SingleRequestStageBinding) {
	plan := SingleRequestStageBinding{Model: "plan-model", Options: map[string]any{"reasoning_effort": "high"}}
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model", Options: map[string]any{"reasoning_effort": "high"}}
	return plan, work, review
}

func TestSingleRequestBindingValid(t *testing.T) {
	plan, work, review := validStages()
	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("valid binding failed: %v", err)
	}
	if b.PublicModel != "virtual-model" {
		t.Errorf("PublicModel=%q, want virtual-model", b.PublicModel)
	}
	if b.WorkspaceRef != "ws-ref" {
		t.Errorf("WorkspaceRef=%q, want ws-ref", b.WorkspaceRef)
	}
	if b.Plan.Model != "plan-model" {
		t.Errorf("Plan.Model=%q, want plan-model", b.Plan.Model)
	}
	if b.Work.Model != "work-model" {
		t.Errorf("Work.Model=%q, want work-model", b.Work.Model)
	}
	if b.Review.Model != "review-model" {
		t.Errorf("Review.Model=%q, want review-model", b.Review.Model)
	}
	if b.Limits.WallClockMS != 30*60*1000 {
		t.Errorf("WallClockMS=%d, want 1800000", b.Limits.WallClockMS)
	}
	if b.Plan.Options["reasoning_effort"] != "high" {
		t.Errorf("Plan.Options missing reasoning_effort=high")
	}
}

func TestSingleRequestBindingRejectsMissingFields(t *testing.T) {
	plan, work, review := validStages()
	limits := validLimits()

	// Test missing public model
	_, err := NewSingleRequestBinding("", "ws-ref", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestMissingPublicModel) {
		t.Errorf("missing public model: got %v, want %v", err, errSingleRequestMissingPublicModel)
	}

	// Test missing workspace ref
	_, err = NewSingleRequestBinding("model", "", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestMissingWorkspaceRef) {
		t.Errorf("missing workspace ref: got %v, want %v", err, errSingleRequestMissingWorkspaceRef)
	}

	// Test missing plan
	_, err = NewSingleRequestBinding("model", "ws", SingleRequestStageBinding{Model: ""}, work, review, limits)
	if !errors.Is(err, errSingleRequestMissingPlan) {
		t.Errorf("missing plan: got %v, want %v", err, errSingleRequestMissingPlan)
	}

	// Test missing work
	_, err = NewSingleRequestBinding("model", "ws", plan, SingleRequestStageBinding{Model: ""}, review, limits)
	if !errors.Is(err, errSingleRequestMissingWork) {
		t.Errorf("missing work: got %v, want %v", err, errSingleRequestMissingWork)
	}

	// Test missing review
	_, err = NewSingleRequestBinding("model", "ws", plan, work, SingleRequestStageBinding{Model: ""}, limits)
	if !errors.Is(err, errSingleRequestMissingReview) {
		t.Errorf("missing review: got %v, want %v", err, errSingleRequestMissingReview)
	}
}

func TestSingleRequestBindingRejectsInvalidLimits(t *testing.T) {
	plan, work, review := validStages()

	// WallClockMS too low
	limits := validLimits()
	limits.WallClockMS = 0
	_, err := NewSingleRequestBinding("model", "ws", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestLimitTooLow) {
		t.Errorf("wall_clock too low: got %v, want %v", err, errSingleRequestLimitTooLow)
	}

	// WallClockMS too high
	limits = validLimits()
	limits.WallClockMS = config.MaxSingleRequestWallClockMS + 1
	_, err = NewSingleRequestBinding("model", "ws", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestLimitTooHigh) {
		t.Errorf("wall_clock too high: got %v, want %v", err, errSingleRequestLimitTooHigh)
	}

	// StageTimeoutMS > WallClockMS: use a stage timeout within valid range
	// but larger than wall clock.
	limits = validLimits()
	limits.WallClockMS = 5 * 60 * 1000    // 5 minutes
	limits.StageTimeoutMS = 6 * 60 * 1000 // 6 minutes — within cap but > wall
	_, err = NewSingleRequestBinding("model", "ws", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestStageTimeoutExceedsWallClock) {
		t.Errorf("stage timeout exceeds wall clock: got %v, want %v", err, errSingleRequestStageTimeoutExceedsWallClock)
	}

	// MaxToolIterations too low
	limits = validLimits()
	limits.MaxToolIterations = 0
	_, err = NewSingleRequestBinding("model", "ws", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestLimitTooLow) {
		t.Errorf("max_tool_iterations too low: got %v, want %v", err, errSingleRequestLimitTooLow)
	}

	// MaxOutputBytes too low
	limits = validLimits()
	limits.MaxOutputBytes = 0
	_, err = NewSingleRequestBinding("model", "ws", plan, work, review, limits)
	if !errors.Is(err, errSingleRequestLimitTooLow) {
		t.Errorf("max_output_bytes too low: got %v, want %v", err, errSingleRequestLimitTooLow)
	}
}

func TestSingleRequestBindingCloneIsolation(t *testing.T) {
	plan, work, review := validStages()
	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("valid binding failed: %v", err)
	}

	clone := b.Clone()
	if clone == nil {
		t.Fatal("Clone returned nil")
	}

	// Verify structural equality
	if clone.PublicModel != b.PublicModel {
		t.Errorf("clone PublicModel=%q, want %q", clone.PublicModel, b.PublicModel)
	}
	if clone.WorkspaceRef != b.WorkspaceRef {
		t.Errorf("clone WorkspaceRef=%q, want %q", clone.WorkspaceRef, b.WorkspaceRef)
	}
	if clone.Plan.Model != b.Plan.Model {
		t.Errorf("clone Plan.Model=%q, want %q", clone.Plan.Model, b.Plan.Model)
	}
	if clone.Work.Model != b.Work.Model {
		t.Errorf("clone Work.Model=%q, want %q", clone.Work.Model, b.Work.Model)
	}
	if clone.Review.Model != b.Review.Model {
		t.Errorf("clone Review.Model=%q, want %q", clone.Review.Model, b.Review.Model)
	}
	if clone.Limits.WallClockMS != b.Limits.WallClockMS {
		t.Errorf("clone WallClockMS=%d, want %d", clone.Limits.WallClockMS, b.Limits.WallClockMS)
	}

	// Mutate clone options — should not affect original
	clone.Plan.Options["reasoning_effort"] = "low"
	if b.Plan.Options["reasoning_effort"] != "high" {
		t.Errorf("original Plan.Options mutated: got %v, want high", b.Plan.Options["reasoning_effort"])
	}

	// Mutate original options — should not affect clone.
	// Work has no options initially, so create one on the original and verify
	// the clone does not see it.
	b.Work.Options = map[string]any{"temperature": 0.7}
	if _, ok := clone.Work.Options["temperature"]; ok {
		t.Errorf("clone Work.Options unexpectedly contains temperature")
	}

	// Mutate clone review options
	clone.Review.Options["top_p"] = 0.9
	if _, ok := b.Review.Options["top_p"]; ok {
		t.Errorf("original Review.Options unexpectedly contains top_p")
	}

	// Nested reference isolation across Clone: nested maps and slices must not
	// alias between the clone and the original in either mutation direction.
	nb, err := NewSingleRequestBinding("virtual-model", "ws-ref",
		SingleRequestStageBinding{Model: "plan-model", Options: map[string]any{"nested": map[string]any{"k": "v"}, "list": []any{"a"}}},
		SingleRequestStageBinding{Model: "work-model"},
		SingleRequestStageBinding{Model: "review-model", Options: map[string]any{"nested": map[string]any{"k": "v"}}},
		validLimits())
	if err != nil {
		t.Fatalf("nested binding failed: %v", err)
	}
	nc := nb.Clone()

	// Mutate nested values on the clone; the original must be unchanged.
	nc.Plan.Options["nested"].(map[string]any)["k"] = "clone-mutated"
	nc.Plan.Options["list"].([]any)[0] = "clone-mutated"
	if got := nb.Plan.Options["nested"].(map[string]any)["k"]; got != "v" {
		t.Errorf("original nested map mutated through clone: got %v, want v", got)
	}
	if got := nb.Plan.Options["list"].([]any)[0]; got != "a" {
		t.Errorf("original nested slice mutated through clone: got %v, want a", got)
	}

	// Mutate a nested value on the original; the clone must be unchanged.
	nb.Review.Options["nested"].(map[string]any)["k"] = "orig-mutated"
	if got := nc.Review.Options["nested"].(map[string]any)["k"]; got != "v" {
		t.Errorf("clone nested map mutated through original: got %v, want v", got)
	}

	// Nil clone returns nil
	var nilB *SingleRequestBinding
	if nilB.Clone() != nil {
		t.Errorf("nil Clone should return nil")
	}
}

func TestSingleRequestBindingDefensiveCopyOptions(t *testing.T) {
	plan, work, review := validStages()
	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("valid binding failed: %v", err)
	}

	// Mutate the original plan options map after binding construction.
	// The binding should have made a defensive copy, so the mutation must
	// not affect the binding's stored options.
	plan.Options["reasoning_effort"] = "low"
	if b.Plan.Options["reasoning_effort"] != "high" {
		t.Errorf("binding Plan.Options mutated by caller: got %v, want high", b.Plan.Options["reasoning_effort"])
	}

	// Mutate the original review options map after binding construction.
	review.Options["top_p"] = 0.5
	if _, ok := b.Review.Options["top_p"]; ok {
		t.Errorf("binding Review.Options mutated by caller: got %v", b.Review.Options)
	}

	// Nested reference isolation: a nested map and slice inside the caller's
	// options must be deep-copied so later caller mutation cannot reach the
	// admitted binding.
	nestedPlan := SingleRequestStageBinding{Model: "plan-model", Options: map[string]any{
		"nested": map[string]any{"k": "v"},
		"list":   []any{"a", "b"},
	}}
	nb, err := NewSingleRequestBinding("virtual-model", "ws-ref", nestedPlan, work, review, validLimits())
	if err != nil {
		t.Fatalf("nested binding failed: %v", err)
	}
	nestedPlan.Options["nested"].(map[string]any)["k"] = "mutated"
	nestedPlan.Options["list"].([]any)[0] = "mutated"
	if got := nb.Plan.Options["nested"].(map[string]any)["k"]; got != "v" {
		t.Errorf("nested map mutated through caller: got %v, want v", got)
	}
	if got := nb.Plan.Options["list"].([]any)[0]; got != "a" {
		t.Errorf("nested slice mutated through caller: got %v, want a", got)
	}
}

func TestSingleRequestBindingDispatchBindingAccepted(t *testing.T) {
	dispatch := SingleRequestStageDispatchBinding{
		Managed:              true,
		ModelGroupKey:        "plan-model",
		RouteID:              "route-plan",
		ProfileID:            "profile-plan",
		CredentialSlotRef:    "slot-plan",
		CredentialRevision:   1,
		RouteRevision:        1,
		PrincipalRef:         "principal-1",
		ProjectionGeneration: 1,
		ProviderID:           "prov-1",
		UpstreamModel:        "served-plan",
		TimeoutSec:           60,
		MaxQueue:             10,
		QueueTimeoutMS:       5000,
	}
	plan := SingleRequestStageBinding{Model: "plan-model", Options: map[string]any{"reasoning_effort": "high"}, Dispatch: &dispatch}
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model", Options: map[string]any{"reasoning_effort": "high"}, Dispatch: &dispatch}

	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("valid binding with dispatch failed: %v", err)
	}
	if b.Plan.Dispatch == nil {
		t.Fatal("expected non-nil Plan.Dispatch")
	}
	if b.Plan.Dispatch.ModelGroupKey != "plan-model" {
		t.Errorf("ModelGroupKey=%q, want plan-model", b.Plan.Dispatch.ModelGroupKey)
	}
	if b.Plan.Dispatch.RouteID != "route-plan" {
		t.Errorf("RouteID=%q, want route-plan", b.Plan.Dispatch.RouteID)
	}
	if b.Plan.Dispatch.ProfileID != "profile-plan" {
		t.Errorf("ProfileID=%q, want profile-plan", b.Plan.Dispatch.ProfileID)
	}
	if b.Plan.Dispatch.PrincipalRef != "principal-1" {
		t.Errorf("PrincipalRef=%q, want principal-1", b.Plan.Dispatch.PrincipalRef)
	}
	if b.Plan.Dispatch.ProviderID != "prov-1" {
		t.Errorf("ProviderID=%q, want prov-1", b.Plan.Dispatch.ProviderID)
	}
	if b.Plan.Dispatch.TimeoutSec != 60 {
		t.Errorf("TimeoutSec=%d, want 60", b.Plan.Dispatch.TimeoutSec)
	}
}

func TestSingleRequestBindingDispatchBindingRejectsMissingFields(t *testing.T) {
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model"}
	base := SingleRequestStageDispatchBinding{
		Managed: true, ModelGroupKey: "plan-model", RouteID: "route-plan",
		ProfileID: "profile-plan", CredentialSlotRef: "slot-plan",
		CredentialRevision: 1, RouteRevision: 1, PrincipalRef: "principal-1",
		ProjectionGeneration: 1, ProviderID: "prov-1", UpstreamModel: "served-plan",
	}

	tests := []struct {
		name string
		mut  func(*SingleRequestStageDispatchBinding)
		err  error
	}{
		{"nil dispatch", func(d *SingleRequestStageDispatchBinding) {}, nil},
		{"managed false", func(d *SingleRequestStageDispatchBinding) { d.Managed = false }, errSingleRequestDispatchMissingManaged},
		{"missing principal ref", func(d *SingleRequestStageDispatchBinding) { d.PrincipalRef = "" }, errSingleRequestDispatchMissingPrincipalRef},
		{"missing model group", func(d *SingleRequestStageDispatchBinding) { d.ModelGroupKey = "" }, errSingleRequestDispatchMissingModelGroupKey},
		{"missing route id", func(d *SingleRequestStageDispatchBinding) { d.RouteID = "" }, errSingleRequestDispatchMissingRouteID},
		{"missing profile id", func(d *SingleRequestStageDispatchBinding) { d.ProfileID = "" }, errSingleRequestDispatchMissingProfileID},
		{"missing credential ref", func(d *SingleRequestStageDispatchBinding) { d.CredentialSlotRef = "" }, errSingleRequestDispatchMissingCredentialRef},
		{"credential revision zero", func(d *SingleRequestStageDispatchBinding) { d.CredentialRevision = 0 }, errSingleRequestDispatchCredentialRevisionTooLow},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			d := base
			if tt.mut != nil {
				tt.mut(&d)
			}
			plan := SingleRequestStageBinding{Model: "plan-model", Dispatch: &d}
			_, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
			if tt.err == nil {
				if err != nil {
					t.Fatalf("expected nil error, got %v", err)
				}
				return
			}
			if !errors.Is(err, tt.err) {
				t.Errorf("error=%v, want %v", err, tt.err)
			}
		})
	}
}

func TestSingleRequestBindingDispatchBindingAcceptsInitialRouteRevision(t *testing.T) {
	dispatch := &SingleRequestStageDispatchBinding{
		Managed: true, ModelGroupKey: "plan-model", RouteID: "route-plan",
		ProfileID: "profile-plan", CredentialSlotRef: "slot-plan",
		CredentialRevision: 1, RouteRevision: 0, PrincipalRef: "principal-1",
		ProjectionGeneration: 1, ProviderID: "prov-1", UpstreamModel: "served-plan",
	}
	binding, err := NewSingleRequestBinding(
		"virtual-model",
		"ws-ref",
		SingleRequestStageBinding{Model: "plan-model", Dispatch: dispatch},
		SingleRequestStageBinding{Model: "work-model"},
		SingleRequestStageBinding{Model: "review-model"},
		validLimits(),
	)
	if err != nil {
		t.Fatalf("initial route revision rejected: %v", err)
	}
	if binding.Plan.Dispatch.RouteRevision != 0 {
		t.Fatalf("RouteRevision=%d, want initial revision 0", binding.Plan.Dispatch.RouteRevision)
	}
}

func TestSingleRequestBindingDispatchBindingCloneIsolation(t *testing.T) {
	dispatch := SingleRequestStageDispatchBinding{
		Managed: true, ModelGroupKey: "plan-model", RouteID: "route-plan",
		ProfileID: "profile-plan", CredentialSlotRef: "slot-plan",
		CredentialRevision: 1, RouteRevision: 1, PrincipalRef: "principal-1",
		ProjectionGeneration: 1, ProviderID: "prov-1", UpstreamModel: "served-plan",
	}
	plan := SingleRequestStageBinding{Model: "plan-model", Dispatch: &dispatch}
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model"}

	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("binding failed: %v", err)
	}

	clone := b.Clone()
	if clone.Plan.Dispatch == nil {
		t.Fatal("clone Plan.Dispatch is nil")
	}

	// Mutate the clone's dispatch; the original must be unchanged.
	clone.Plan.Dispatch.ModelGroupKey = "mutated"
	if b.Plan.Dispatch.ModelGroupKey != "plan-model" {
		t.Errorf("original ModelGroupKey mutated through clone: got %q", b.Plan.Dispatch.ModelGroupKey)
	}

	// Mutate the original's dispatch; the clone must be unchanged.
	b.Plan.Dispatch.ProviderID = "mutated-prov"
	if clone.Plan.Dispatch.ProviderID != "prov-1" {
		t.Errorf("clone ProviderID mutated through original: got %q", clone.Plan.Dispatch.ProviderID)
	}
}

func TestSingleRequestBindingDispatchCredentialBindingSnapshot(t *testing.T) {
	dispatch := SingleRequestStageDispatchBinding{
		Managed: true, ModelGroupKey: "plan-model", RouteID: "route-plan",
		ProfileID: "profile-plan", CredentialSlotRef: "slot-plan",
		CredentialRevision: 1, RouteRevision: 1, PrincipalRef: "principal-1",
		ProjectionGeneration: 1,
	}

	cb := dispatch.CredentialBindingSnapshot()
	if cb == nil {
		t.Fatal("expected non-nil credential binding snapshot")
	}
	if cb.PrincipalRef != "principal-1" {
		t.Errorf("PrincipalRef=%q, want principal-1", cb.PrincipalRef)
	}
	if cb.CredentialSlotRef != "slot-plan" {
		t.Errorf("CredentialSlotRef=%q, want slot-plan", cb.CredentialSlotRef)
	}
	if cb.RouteID != "route-plan" {
		t.Errorf("RouteID=%q, want route-plan", cb.RouteID)
	}

	// Unmanaged dispatch returns nil.
	noManaged := SingleRequestStageDispatchBinding{Managed: false}
	if noManaged.CredentialBindingSnapshot() != nil {
		t.Error("unmanaged dispatch should return nil credential binding")
	}

	// Nil dispatch returns nil.
	var nilDispatch *SingleRequestStageDispatchBinding
	if nilDispatch.CredentialBindingSnapshot() != nil {
		t.Error("nil dispatch should return nil credential binding")
	}
}

func TestSingleRequestBindingDispatchBindingMutationAfterAdmission(t *testing.T) {
	dispatch := SingleRequestStageDispatchBinding{
		Managed: true, ModelGroupKey: "plan-model", RouteID: "route-plan",
		ProfileID: "profile-plan", CredentialSlotRef: "slot-plan",
		CredentialRevision: 1, RouteRevision: 1, PrincipalRef: "principal-1",
		ProjectionGeneration: 1, ProviderID: "prov-1", UpstreamModel: "served-plan",
	}
	plan := SingleRequestStageBinding{Model: "plan-model", Dispatch: &dispatch}
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model"}

	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("binding failed: %v", err)
	}

	// Mutate the original dispatch after admission. The admitted binding must
	// not reflect the mutation.
	dispatch.ModelGroupKey = "mutated-after-admission"
	dispatch.ProviderID = "mutated-provider"
	if b.Plan.Dispatch.ModelGroupKey != "plan-model" {
		t.Errorf("admitted ModelGroupKey reflected mutation: got %q", b.Plan.Dispatch.ModelGroupKey)
	}
	if b.Plan.Dispatch.ProviderID != "prov-1" {
		t.Errorf("admitted ProviderID reflected mutation: got %q", b.Plan.Dispatch.ProviderID)
	}
}

// customPlanTemplate and customReviewTemplate are operator-authored effective
// templates. They differ from the built-in defaults so an admitted snapshot
// cannot pass by accidentally falling back.
const (
	customPlanTemplate = `# Plan

Operator preamble.

## Goal
{{goal}}

## Steps
{{steps}}

## Verification
{{verification}}
`
	customReviewTemplate = `# Review

Operator preamble.

## Worker Item Status
{{item_status}}

## Worker Changes
{{changes}}

## Worker Verification
{{verification}}

## Deviations
{{deviations}}
`
)

// TestSingleRequestBindingTemplateSnapshot proves the effective Plan/Review
// template pair is frozen at admission and carried unchanged through Clone and
// the workspace revalidation path, and that an invalid template cannot be
// admitted.
func TestSingleRequestBindingTemplateSnapshot(t *testing.T) {
	plan, work, review := validStages()

	t.Run("admitted binding carries the exact effective pair", func(t *testing.T) {
		b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
		if err != nil {
			t.Fatalf("custom template admission failed: %v", err)
		}
		if b.Templates.Plan != customPlanTemplate {
			t.Errorf("Templates.Plan = %q, want the configured custom Plan template", b.Templates.Plan)
		}
		if b.Templates.Review != customReviewTemplate {
			t.Errorf("Templates.Review = %q, want the configured custom Review template", b.Templates.Review)
		}
	})

	t.Run("template-less admission falls back to the built-in defaults", func(t *testing.T) {
		b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
		if err != nil {
			t.Fatalf("default admission failed: %v", err)
		}
		if b.Templates.Plan != singlerequesttemplate.DefaultPlanTemplate {
			t.Errorf("Templates.Plan = %q, want the built-in default", b.Templates.Plan)
		}
		if b.Templates.Review != singlerequesttemplate.DefaultReviewTemplate {
			t.Errorf("Templates.Review = %q, want the built-in default", b.Templates.Review)
		}
	})

	t.Run("clone preserves the frozen pair", func(t *testing.T) {
		b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
		if err != nil {
			t.Fatalf("custom template admission failed: %v", err)
		}
		clone := b.Clone()
		if clone.Templates != b.Templates {
			t.Fatalf("clone templates = %+v, want %+v", clone.Templates, b.Templates)
		}
		// Mutating the clone's snapshot must not reach the admitted binding.
		clone.Templates.Plan = singlerequesttemplate.DefaultPlanTemplate
		if b.Templates.Plan != customPlanTemplate {
			t.Errorf("admitted Templates.Plan mutated through the clone: %q", b.Templates.Plan)
		}
	})

	t.Run("workspace revalidation preserves the frozen pair", func(t *testing.T) {
		b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
		if err != nil {
			t.Fatalf("custom template admission failed: %v", err)
		}
		b.Workspace = &SingleRequestWorkspaceBinding{
			Ref:                  "ws-ref",
			NodeID:               "node-1",
			ConnectionGeneration: 7,
			OperationIDs:         []string{"list", "read"},
			Limits:               SingleRequestWorkspaceLimits{MaxReadBytes: 1024, MaxOutputBytes: 2048},
		}

		revalidated, err := cloneValidatedSingleRequestBinding(b)
		if err != nil {
			t.Fatalf("workspace revalidation failed: %v", err)
		}
		if revalidated.Templates.Plan != customPlanTemplate || revalidated.Templates.Review != customReviewTemplate {
			t.Fatalf("revalidated templates = %+v, want the admitted pair", revalidated.Templates)
		}
		if revalidated.Workspace == nil || revalidated.Workspace.ConnectionGeneration != 7 {
			t.Fatalf("revalidated workspace = %+v, want the frozen generation", revalidated.Workspace)
		}
	})

	t.Run("invalid effective templates are rejected at admission", func(t *testing.T) {
		if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: "### Plan\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n", Review: customReviewTemplate}); err == nil {
			t.Error("expected rejection for a decorated Plan heading")
		}
		if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: strings.Replace(customReviewTemplate, "{{deviations}}", "{{summary}}", 1)}); err == nil {
			t.Error("expected rejection for a legacy Review placeholder")
		}
		if _, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: "", Review: ""}); err == nil {
			t.Error("expected rejection for empty effective templates")
		}
	})

	t.Run("an invalid template cannot survive workspace revalidation", func(t *testing.T) {
		b, err := NewSingleRequestBindingWithTemplates("virtual-model", "ws-ref", plan, work, review, validLimits(),
			SingleRequestTemplateBinding{Plan: customPlanTemplate, Review: customReviewTemplate})
		if err != nil {
			t.Fatalf("custom template admission failed: %v", err)
		}
		// Simulate post-admission tampering: revalidation must reject it rather
		// than clone a malformed template forward.
		b.Templates.Review = strings.Replace(customReviewTemplate, "{{deviations}}", "{{summary}}", 1)
		if _, err := cloneValidatedSingleRequestBinding(b); err == nil {
			t.Error("expected workspace revalidation to reject a tampered Review template")
		}
	})
}

func TestSingleRequestBindingDispatchNilIsAllowed(t *testing.T) {
	// A stage without dispatch binding is valid (e.g. unmanaged preset fallback).
	plan := SingleRequestStageBinding{Model: "plan-model"}
	work := SingleRequestStageBinding{Model: "work-model"}
	review := SingleRequestStageBinding{Model: "review-model"}

	b, err := NewSingleRequestBinding("virtual-model", "ws-ref", plan, work, review, validLimits())
	if err != nil {
		t.Fatalf("binding with nil dispatch failed: %v", err)
	}
	if b.Plan.Dispatch != nil {
		t.Errorf("expected nil Plan.Dispatch, got %+v", b.Plan.Dispatch)
	}
}
