package execution_test

import (
	"context"
	"errors"
	"fmt"
	"math"
	"testing"
	"time"

	"iop/packages/go/execution"
)

func TestResolveStallTimeoutMS(t *testing.T) {
	cases := []struct {
		name    string
		ms      int64
		want    int64
		wantErr bool
	}{
		{"zero maps to default", 0, execution.DefaultResponseStallTimeoutMS, false},
		{"default passes through", execution.DefaultResponseStallTimeoutMS, execution.DefaultResponseStallTimeoutMS, false},
		{"custom positive passes through", 60000, 60000, false},
		{"small positive passes through", 1, 1, false},
		{"exact safe boundary passes through", math.MaxInt64 / int64(time.Millisecond), math.MaxInt64 / int64(time.Millisecond), false},
		{"first overflowing millisecond rejected", math.MaxInt64/int64(time.Millisecond) + 1, 0, true},
		{"negative rejected", -1, 0, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := execution.ResolveStallTimeoutMS(tc.ms)
			if (err != nil) != tc.wantErr {
				t.Fatalf("ResolveStallTimeoutMS(%d) error = %v, want error=%t", tc.ms, err, tc.wantErr)
			}
			if !tc.wantErr && got != tc.want {
				t.Errorf("ResolveStallTimeoutMS(%d) = %d, want %d", tc.ms, got, tc.want)
			}
		})
	}
}

func TestValidateStallTimeoutMS(t *testing.T) {
	if err := execution.ValidateStallTimeoutMS(300000); err != nil {
		t.Errorf("expected nil for 300000, got %v", err)
	}
	if err := execution.ValidateStallTimeoutMS(1); err != nil {
		t.Errorf("expected nil for 1, got %v", err)
	}
	if err := execution.ValidateStallTimeoutMS(0); err != nil {
		t.Errorf("expected nil for 0 (use default), got %v", err)
	}
	if err := execution.ValidateStallTimeoutMS(-1); err == nil {
		t.Error("expected error for -1")
	}
	if err := execution.ValidateStallTimeoutMS(math.MaxInt64/int64(time.Millisecond) + 1); err == nil {
		t.Error("expected error for overflow value")
	}
}

func TestStallTimeoutValidationError(t *testing.T) {
	e := &execution.StallTimeoutValidationError{Value: -1, Msg: "must be positive"}
	if e.Error() != "must be positive" {
		t.Errorf("Error() = %q, want 'must be positive'", e.Error())
	}
	e2 := &execution.StallTimeoutValidationError{Value: 0}
	if e2.Error() != "invalid response_stall_timeout_ms" {
		t.Errorf("Error() = %q, want 'invalid response_stall_timeout_ms'", e2.Error())
	}
}

func TestClassifyRuntimeEvent(t *testing.T) {
	now := time.Now()
	cases := []struct {
		name string
		ev   execution.RuntimeEvent
		want execution.ProviderActivityDisposition
	}{
		{
			name: "start event",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeStart, RunID: "r1", Timestamp: now},
			want: execution.DispositionStart,
		},
		{
			name: "delta with text",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeDelta, Delta: "hello", RunID: "r1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "delta with message",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeDelta, Message: "hi", RunID: "r1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "reasoning_delta with text",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeReasoningDelta, Delta: "thinking...", RunID: "r1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{name: "delta with zero usage", ev: execution.RuntimeEvent{Type: execution.EventTypeDelta, Usage: &execution.UsageStats{}, RunID: "r1", Timestamp: now}, want: execution.DispositionProgress},
		{name: "delta with token usage", ev: execution.RuntimeEvent{Type: execution.EventTypeDelta, Usage: &execution.UsageStats{OutputTokens: 1}, RunID: "r1", Timestamp: now}, want: execution.DispositionProgress},
		{name: "reasoning delta with token usage", ev: execution.RuntimeEvent{Type: execution.EventTypeReasoningDelta, Usage: &execution.UsageStats{ReasoningTokens: 1}, RunID: "r1", Timestamp: now}, want: execution.DispositionProgress},
		{name: "delta empty no usage", ev: execution.RuntimeEvent{Type: execution.EventTypeDelta, RunID: "r1", Timestamp: now}, want: execution.DispositionNone},
		{
			name: "complete with usage",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeComplete, Usage: &execution.UsageStats{OutputTokens: 10}, RunID: "r1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
		{
			name: "complete without usage",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeComplete, RunID: "r1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
		{name: "error with payload and usage", ev: execution.RuntimeEvent{Type: execution.EventTypeError, Delta: "last", Error: "boom", Usage: &execution.UsageStats{OutputTokens: 1}, RunID: "r1", Timestamp: now}, want: execution.DispositionTerminal},
		{name: "cancelled with payload and usage", ev: execution.RuntimeEvent{Type: execution.EventTypeCancelled, Message: "last", Usage: &execution.UsageStats{InputTokens: 1}, RunID: "r1", Timestamp: now}, want: execution.DispositionTerminal},
		{
			name: "unknown type",
			ev:   execution.RuntimeEvent{Type: "unknown", RunID: "r1", Timestamp: now},
			want: execution.DispositionNone,
		},
		{
			name: "delta with terminal usage takes terminal",
			ev:   execution.RuntimeEvent{Type: execution.EventTypeComplete, Delta: "last", Usage: &execution.UsageStats{OutputTokens: 5}, RunID: "r1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := execution.ClassifyRuntimeEvent(tc.ev)
			if got != tc.want {
				t.Errorf("ClassifyRuntimeEvent: got %q, want %q", got, tc.want)
			}
		})
	}
}

func TestClassifyProviderTunnelFrame(t *testing.T) {
	now := time.Now()
	cases := []struct {
		name string
		f    execution.ProviderTunnelFrame
		want execution.ProviderActivityDisposition
	}{
		{
			name: "response_start",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindResponseStart, RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "response_start with headers",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindResponseStart, Headers: map[string]string{"content-type": "text/event-stream"}, RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "body with data",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindBody, Body: []byte("hello"), RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "body empty",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindBody, RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionNone,
		},
		{
			name: "end",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindEnd, RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
		{
			name: "error",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindError, Error: "provider timeout", RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
		{
			name: "usage with tokens",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindUsage, Usage: &execution.UsageStats{OutputTokens: 10}, RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionProgress,
		},
		{
			name: "unknown kind",
			f:    execution.ProviderTunnelFrame{Kind: "bogus", RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionNone,
		},
		{
			name: "end with body takes terminal",
			f:    execution.ProviderTunnelFrame{Kind: execution.ProviderTunnelFrameKindEnd, Body: []byte("final"), RunID: "r1", TunnelID: "t1", Timestamp: now},
			want: execution.DispositionTerminal,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := execution.ClassifyProviderTunnelFrame(tc.f)
			if got != tc.want {
				t.Errorf("ClassifyProviderTunnelFrame: got %q, want %q", got, tc.want)
			}
		})
	}
}

// assertProbeOutcome checks both the liveness classification and the normalized
// health for a probe outcome, keeping the table-driven probe tests compact.
func assertProbeOutcome(t *testing.T, outcome execution.ProbeOutcome, wantClass execution.LivenessClassification, wantHealth execution.ProviderHealth) {
	t.Helper()
	if gotClass := execution.ClassifyProbeOutcome(outcome); gotClass != wantClass {
		t.Errorf("ClassifyProbeOutcome: got %q, want %q", gotClass, wantClass)
	}
	if gotHealth := execution.NormalizeProbeOutcome(outcome); gotHealth != wantHealth {
		t.Errorf("NormalizeProbeOutcome: got %q, want %q", gotHealth, wantHealth)
	}
}

func TestClassifyProbeOutcomeDefinitive(t *testing.T) {
	cases := []struct {
		name       string
		outcome    execution.ProbeOutcome
		wantClass  execution.LivenessClassification
		wantHealth execution.ProviderHealth
	}{
		{
			name: "matching available",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a",
				Status: execution.ProviderStatusAvailable,
			},
			wantClass: execution.LivenessAvailable, wantHealth: execution.RequestStalled,
		},
		{
			name: "matching unavailable",
			outcome: execution.ProbeOutcome{
				AdapterName: "ollama", ExpectedAdapter: "ollama",
				Target: "m-b", ExpectedTarget: "m-b",
				Status: execution.ProviderStatusUnavailable,
			},
			wantClass: execution.LivenessUnavailable, wantHealth: execution.ProviderUnhealthy,
		},
		{
			name: "matching available with pinned instance",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				InstanceKey: "vllm-gpu", ExpectedInstance: "vllm-gpu",
				Target: "m-a", ExpectedTarget: "m-a",
				Status: execution.ProviderStatusAvailable,
			},
			wantClass: execution.LivenessAvailable, wantHealth: execution.RequestStalled,
		},
		{
			name: "pinned instance mismatch stays inconclusive",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				InstanceKey: "vllm-gpu", ExpectedInstance: "vllm-other",
				Target: "m-a", ExpectedTarget: "m-a",
				Status: execution.ProviderStatusAvailable,
			},
			wantClass: execution.LivenessIdentityMismatch, wantHealth: execution.HealthUnknown,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assertProbeOutcome(t, tc.outcome, tc.wantClass, tc.wantHealth)
		})
	}
}

func TestClassifyProbeOutcomeInconclusive(t *testing.T) {
	cases := []struct {
		name       string
		outcome    execution.ProbeOutcome
		wantClass  execution.LivenessClassification
		wantHealth execution.ProviderHealth
	}{
		{
			name: "transport error takes precedence over available status",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a",
				Status: execution.ProviderStatusAvailable, Err: errors.New("boom"),
			},
			wantClass: execution.LivenessError, wantHealth: execution.HealthUnknown,
		},
		{
			name: "deadline exceeded is timeout",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a",
				Err: context.DeadlineExceeded,
			},
			wantClass: execution.LivenessTimeout, wantHealth: execution.HealthUnknown,
		},
		{
			name: "cancellation is timeout",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a",
				Err: context.Canceled,
			},
			wantClass: execution.LivenessTimeout, wantHealth: execution.HealthUnknown,
		},
		{
			name: "unsupported adapter",
			outcome: execution.ProbeOutcome{
				AdapterName: "worker", ExpectedAdapter: "worker",
				Target: "m-a", ExpectedTarget: "m-a",
				Err: execution.ErrProbeUnsupported,
			},
			wantClass: execution.LivenessUnsupported, wantHealth: execution.HealthUnknown,
		},
		{
			name: "wrapped unsupported is still unsupported",
			outcome: execution.ProbeOutcome{
				AdapterName: "worker", ExpectedAdapter: "worker",
				Target: "m-a", ExpectedTarget: "m-a",
				Err: fmt.Errorf("resolve: %w", execution.ErrProbeUnsupported),
			},
			wantClass: execution.LivenessUnsupported, wantHealth: execution.HealthUnknown,
		},
		{
			name: "unknown status",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a",
				Status: execution.ProviderStatusUnknown,
			},
			wantClass: execution.LivenessUnknown, wantHealth: execution.HealthUnknown,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assertProbeOutcome(t, tc.outcome, tc.wantClass, tc.wantHealth)
		})
	}
}

func TestClassifyProbeOutcomeIdentity(t *testing.T) {
	avail := execution.ProviderStatusAvailable
	mismatch := execution.LivenessIdentityMismatch
	cases := []struct {
		name    string
		outcome execution.ProbeOutcome
	}{
		{
			name: "empty adapter identity",
			outcome: execution.ProbeOutcome{
				AdapterName: "", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a", Status: avail,
			},
		},
		{
			name: "empty expected adapter",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "",
				Target: "m-a", ExpectedTarget: "m-a", Status: avail,
			},
		},
		{
			name: "empty target identity",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "", ExpectedTarget: "m-a", Status: avail,
			},
		},
		{
			name: "mismatched adapter",
			outcome: execution.ProbeOutcome{
				AdapterName: "ollama", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-a", Status: avail,
			},
		},
		{
			name: "mismatched target",
			outcome: execution.ProbeOutcome{
				AdapterName: "vllm", ExpectedAdapter: "vllm",
				Target: "m-a", ExpectedTarget: "m-b", Status: avail,
			},
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			assertProbeOutcome(t, tc.outcome, mismatch, execution.HealthUnknown)
		})
	}
}

func TestHealthFromClassification(t *testing.T) {
	cases := []struct {
		class execution.LivenessClassification
		want  execution.ProviderHealth
	}{
		{execution.LivenessAvailable, execution.RequestStalled},
		{execution.LivenessUnavailable, execution.ProviderUnhealthy},
		{execution.LivenessTimeout, execution.HealthUnknown},
		{execution.LivenessError, execution.HealthUnknown},
		{execution.LivenessUnsupported, execution.HealthUnknown},
		{execution.LivenessUnknown, execution.HealthUnknown},
		{execution.LivenessIdentityMismatch, execution.HealthUnknown},
		{execution.LivenessClassification("bogus"), execution.HealthUnknown},
	}
	for _, tc := range cases {
		if got := execution.HealthFromClassification(tc.class); got != tc.want {
			t.Errorf("HealthFromClassification(%q): got %q, want %q", tc.class, got, tc.want)
		}
	}
}
