package node

import (
	"reflect"
	"testing"
	"time"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/structpb"

	runtime "iop/packages/go/execution"
	iop "iop/proto/gen/iop"
)

func TestRunRequestFromProtoPreservesWireFields(t *testing.T) {
	policy, err := structpb.NewStruct(map[string]any{"allow": true})
	if err != nil {
		t.Fatal(err)
	}
	input, err := structpb.NewStruct(map[string]any{"prompt": "hello"})
	if err != nil {
		t.Fatal(err)
	}
	wire := &iop.RunRequest{
		RunId:      "run-1",
		Adapter:    "ollama",
		Target:     "model",
		SessionId:  "correlation-1",
		Background: true,
		Policy:     policy,
		Input:      input,
		TimeoutSec: 30,
		Metadata:   map[string]string{"source": "test"},
	}

	got := runRequestFromProto(wire)
	if got.RunID != wire.RunId || got.Adapter != wire.Adapter || got.Target != wire.Target ||
		got.SessionID != wire.SessionId || got.Background != wire.Background ||
		got.TimeoutSec != int(wire.TimeoutSec) {
		t.Fatalf("runRequestFromProto() = %#v", got)
	}
	if !reflect.DeepEqual(got.Policy, policy.AsMap()) || !reflect.DeepEqual(got.Input, input.AsMap()) ||
		!reflect.DeepEqual(got.Metadata, wire.Metadata) {
		t.Fatalf("mapped maps = policy %#v input %#v metadata %#v", got.Policy, got.Input, got.Metadata)
	}
}

func TestRunEventToProtoPreservesLegacyValues(t *testing.T) {
	timestamp := time.Unix(0, 1234)
	event := runtime.RuntimeEvent{
		RunID:    "run-1",
		Type:     runtime.EventTypeError,
		Error:    "legacy error",
		Failure:  &runtime.Failure{Code: runtime.FailureCodeProvider, Message: "typed error"},
		Metadata: map[string]string{"key": "value"},
		Usage: &runtime.UsageStats{
			InputTokens:       1,
			OutputTokens:      2,
			ReasoningTokens:   3,
			CachedInputTokens: 4,
		},
		Timestamp: timestamp,
	}

	got := runEventToProto(event, "node-1", "session-1", true)
	if got.GetRunId() != "run-1" || got.GetType() != "error" || got.GetError() != "legacy error" ||
		got.GetNodeId() != "node-1" || got.GetSessionId() != "session-1" || !got.GetBackground() ||
		got.GetTimestamp() != timestamp.UnixNano() {
		t.Fatalf("runEventToProto() = %#v", got)
	}
	if got.GetUsage().GetInputTokens() != 1 || got.GetUsage().GetOutputTokens() != 2 ||
		got.GetUsage().GetReasoningTokens() != 3 || got.GetUsage().GetCachedInputTokens() != 4 {
		t.Fatalf("usage = %#v", got.GetUsage())
	}
	if !reflect.DeepEqual(got.GetMetadata(), event.Metadata) {
		t.Fatalf("metadata = %#v, want %#v", got.GetMetadata(), event.Metadata)
	}
}

func TestRunEventToProtoUsesTypedFailureMessageAsFallback(t *testing.T) {
	got := runEventToProto(runtime.RuntimeEvent{
		RunID:     "run-1",
		Type:      runtime.EventTypeError,
		Failure:   &runtime.Failure{Code: runtime.FailureCodeUnavailable, Message: "unavailable"},
		Timestamp: time.Unix(0, 1),
	}, "node-1", "default", false)
	if got.GetError() != "unavailable" {
		t.Fatalf("error = %q, want unavailable", got.GetError())
	}
}

func TestRunRequestFromProtoLeavesRawStallTimeoutForHandlerValidation(t *testing.T) {
	// The handler, not the protobuf mapper, validates and resolves the raw value.
	req := &iop.RunRequest{
		RunId:   "r1",
		Adapter: "ollama",
	}
	runtimeReq := runRequestFromProto(req)
	if runtimeReq.ResponseStallTimeoutMS != 0 {
		t.Errorf("zero wire must remain raw before handler validation, got %d", runtimeReq.ResponseStallTimeoutMS)
	}

	// Positive override → passes through.
	req2 := &iop.RunRequest{
		RunId:                  "r2",
		Adapter:                "ollama",
		ResponseStallTimeoutMs: 60000,
	}
	runtimeReq2 := runRequestFromProto(req2)
	if runtimeReq2.ResponseStallTimeoutMS != 0 {
		t.Errorf("positive wire value must remain raw before handler validation, got %d", runtimeReq2.ResponseStallTimeoutMS)
	}
}

func TestValidateStallTimeoutOnWire(t *testing.T) {
	cases := []struct {
		name string
		raw  int64
		want int64
		bad  bool
	}{
		{name: "zero defaults", raw: 0, want: runtime.DefaultResponseStallTimeoutMS},
		{name: "positive preserved", raw: 60000, want: 60000},
		{name: "negative rejected", raw: -1, bad: true},
		{name: "overflow rejected", raw: 99999999999999, bad: true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got, err := ValidateStallTimeoutOnWire(tc.raw)
			if (err != nil) != tc.bad {
				t.Fatalf("ValidateStallTimeoutOnWire(%d) error = %v, want bad=%t", tc.raw, err, tc.bad)
			}
			if !tc.bad && got != tc.want {
				t.Errorf("ValidateStallTimeoutOnWire(%d) = %d, want %d", tc.raw, got, tc.want)
			}
		})
	}
}

func TestResponseStallTimeoutWireRoundTrip(t *testing.T) {
	values := []int64{0, 60000, -1, (int64(1) << 62) / int64(time.Millisecond)}
	for _, value := range values {
		t.Run("run", func(t *testing.T) {
			original := &iop.RunRequest{ResponseStallTimeoutMs: value}
			encoded, err := proto.Marshal(original)
			if err != nil {
				t.Fatal(err)
			}
			decoded := &iop.RunRequest{}
			if err := proto.Unmarshal(encoded, decoded); err != nil {
				t.Fatal(err)
			}
			if decoded.GetResponseStallTimeoutMs() != value {
				t.Fatalf("RunRequest round trip = %d, want %d", decoded.GetResponseStallTimeoutMs(), value)
			}
		})
		t.Run("tunnel", func(t *testing.T) {
			original := &iop.ProviderTunnelRequest{ResponseStallTimeoutMs: value}
			encoded, err := proto.Marshal(original)
			if err != nil {
				t.Fatal(err)
			}
			decoded := &iop.ProviderTunnelRequest{}
			if err := proto.Unmarshal(encoded, decoded); err != nil {
				t.Fatal(err)
			}
			if decoded.GetResponseStallTimeoutMs() != value {
				t.Fatalf("ProviderTunnelRequest round trip = %d, want %d", decoded.GetResponseStallTimeoutMs(), value)
			}
		})
	}
}

func TestRuntimeEventToProtoPreservesTypedFailure(t *testing.T) {
	t.Run("stalled failure populated with allowlisted metadata", func(t *testing.T) {
		inputMeta := map[string]string{
			"failure_code":            "response_stalled",
			"provider_health":         "available",
			"liveness_classification": "request_stalled",
			"idle_duration_ms":        "5000",
			"run_id":                  "run-1",
			"attempt_id":              "run-1",
			"attempt_fence":           "confirmed",
			"adapter":                 "ollama",
			"target":                  "llama3",
			"health_observation_seq":  "1",
			"recovery_eligible":       "true",
			"secret_key":              "sensitive",
		}
		event := runtime.RuntimeEvent{
			RunID: eventTypeStalledRunID(),
			Type:  runtime.EventTypeError,
			Error: "provider response stalled",
			Failure: &runtime.Failure{
				Code:      runtime.FailureCodeResponseStalled,
				Message:   "provider response stalled",
				Retryable: true,
				Metadata:  inputMeta,
			},
		}

		wire := runEventToProto(event, "node-1", "session-1", false)

		if wire.GetFailure() == nil {
			t.Fatal("expected non-nil wire.Failure")
		}
		if wire.GetFailure().GetCode() != "response_stalled" {
			t.Fatalf("code = %q, want response_stalled", wire.GetFailure().GetCode())
		}
		if wire.GetFailure().GetMessage() != "provider response stalled" {
			t.Fatalf("message = %q", wire.GetFailure().GetMessage())
		}
		if !wire.GetFailure().GetRetryable() {
			t.Fatal("expected retryable = true")
		}

		meta := wire.GetFailure().GetMetadata()
		if meta["provider_health"] != "available" || meta["liveness_classification"] != "request_stalled" || meta["health_observation_seq"] != "1" {
			t.Fatalf("allowlisted metadata missing or invalid = %#v", meta)
		}
		if meta["recovery_eligible"] != "" || meta["secret_key"] != "" {
			t.Fatalf("non-allowlisted metadata present in wire failure: %#v", meta)
		}

		// Verify defensive cloning: mutating input map must not alter wire failure metadata
		inputMeta["attempt_fence"] = "mutated"
		if meta["attempt_fence"] != "confirmed" {
			t.Fatal("wire failure metadata shared mutable alias with input metadata")
		}
	})

	t.Run("non-stalled failure leaves wire failure nil", func(t *testing.T) {
		event := runtime.RuntimeEvent{
			RunID:   "run-2",
			Type:    runtime.EventTypeError,
			Error:   "cancelled error",
			Failure: &runtime.Failure{Code: runtime.FailureCodeCancelled, Message: "cancelled error"},
		}
		wire := runEventToProto(event, "node-1", "session-1", false)
		if wire.GetFailure() != nil {
			t.Fatalf("expected nil wire.Failure for non-stalled code, got %#v", wire.GetFailure())
		}
		if wire.GetError() != "cancelled error" {
			t.Fatalf("error string = %q, want cancelled error", wire.GetError())
		}
	})

	t.Run("nil failure leaves wire failure nil", func(t *testing.T) {
		event := runtime.RuntimeEvent{
			RunID: "run-3",
			Type:  runtime.EventTypeComplete,
		}
		wire := runEventToProto(event, "node-1", "session-1", false)
		if wire.GetFailure() != nil {
			t.Fatalf("expected nil wire.Failure for nil failure, got %#v", wire.GetFailure())
		}
	})
}

func eventTypeStalledRunID() string { return "run-1" }
