package node

import (
	"reflect"
	"strings"
	"testing"

	"google.golang.org/protobuf/types/known/structpb"
	"iop/packages/go/config"
)

// TestConcreteProfileToProtoDeepCopy verifies that concreteProfileToProto
// deep-copies maps so the wire payload is independent of the source config
// snapshot.
func TestProtocolProfileConcreteToProtoDeepCopy(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "openai",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver:  config.ProtocolDriverOpenAIChat,
			BaseURL: "https://api.openai.com/v1",
			Operations: map[string]string{
				"models":           "/models",
				"chat_completions": "/chat/completions",
			},
			Auth:         config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
			Capabilities: []string{"chat", "streaming"},
			ModelMapping: map[string]string{
				"gpt-4": "gpt-4",
			},
		},
	}

	proto := concreteProfileToProto(&profile)
	if proto == nil {
		t.Fatal("expected non-nil proto")
	}
	if proto.GetId() != "openai" {
		t.Errorf("Id = %q, want %q", proto.GetId(), "openai")
	}
	if proto.GetDriver() != string(config.ProtocolDriverOpenAIChat) {
		t.Errorf("Driver = %q, want %q", proto.GetDriver(), config.ProtocolDriverOpenAIChat)
	}
	if proto.GetBaseUrl() != "https://api.openai.com/v1" {
		t.Errorf("BaseUrl = %q, want %q", proto.GetBaseUrl(), "https://api.openai.com/v1")
	}
	if proto.GetAuth().GetHeader() != "Authorization" {
		t.Errorf("Auth.Header = %q, want %q", proto.GetAuth().GetHeader(), "Authorization")
	}
	if len(proto.GetOperations()) != 2 {
		t.Errorf("Operations len = %d, want 2", len(proto.GetOperations()))
	}
	if len(proto.GetCapabilities()) != 2 {
		t.Errorf("Capabilities len = %d, want 2", len(proto.GetCapabilities()))
	}

	// Mutate the proto maps and verify the source profile is unaffected.
	proto.Operations["injected"] = "/injected"
	if _, ok := profile.Operations["injected"]; ok {
		t.Error("mutating proto operations affected source profile")
	}
	proto.Capabilities = append(proto.Capabilities, "injected")
	if len(profile.Capabilities) != 2 {
		t.Error("mutating proto capabilities affected source profile")
	}
}

// TestConcreteProfileToProtoNil verifies that a nil profile produces a nil
// proto, preserving legacy payloads without a profile.
func TestProtocolProfileConcreteToProtoNil(t *testing.T) {
	if proto := concreteProfileToProto(nil); proto != nil {
		t.Errorf("expected nil proto for nil profile, got %v", proto)
	}
}

// TestConcreteProfileToProtoNoBase verifies that the concrete proto snapshot
// never carries a Base field across the wire.
func TestProtocolProfileConcreteToProtoNoBase(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "custom",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Base:    "openai",
			Driver:  config.ProtocolDriverOpenAIChat,
			BaseURL: "https://proxy.example.invalid/v1",
			Operations: map[string]string{
				"models": "/models",
			},
			Auth:         config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
			Capabilities: []string{"chat"},
		},
	}

	proto := concreteProfileToProto(&profile)
	if proto == nil {
		t.Fatal("expected non-nil proto")
	}
	// The ConcreteProtocolProfile proto message has no Base field, so there
	// is nothing to assert beyond confirming the snapshot resolves correctly.
	if proto.GetId() != "custom" {
		t.Errorf("Id = %q, want %q", proto.GetId(), "custom")
	}
	if proto.GetBaseUrl() != "https://proxy.example.invalid/v1" {
		t.Errorf("BaseUrl = %q, want overridden value", proto.GetBaseUrl())
	}
}

// TestConcreteProfileToProtoExtensions verifies that extensions are carried
// without panicking even when the struct conversion is partial.
func TestProtocolProfileConcreteToProtoExtensions(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "ext",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver:  config.ProtocolDriverOpenAIChat,
			BaseURL: "https://api.example.com/v1",
			Operations: map[string]string{
				"models": "/models",
			},
			Auth:         config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
			Capabilities: []string{"chat"},
			Extensions: map[string]any{
				"key": "value",
				"nested": map[string]any{
					"enabled": true,
					"items":   []any{"one", map[string]any{"leaf": "two"}},
				},
			},
		},
	}

	proto := concreteProfileToProto(&profile)
	if proto == nil {
		t.Fatal("expected non-nil proto")
	}
	// Extensions are converted to a structpb.Struct; verify it is non-nil
	// when the source has extensions.
	if proto.GetExtensions() == nil {
		t.Error("expected non-nil extensions struct")
	}
	want := map[string]any{
		"key": "value",
		"nested": map[string]any{
			"enabled": true,
			"items":   []any{"one", map[string]any{"leaf": "two"}},
		},
	}
	if got := proto.GetExtensions().AsMap(); !reflect.DeepEqual(got, want) {
		t.Fatalf("extensions = %#v, want %#v", got, want)
	}
	profile.Extensions["nested"].(map[string]any)["enabled"] = false
	if got := proto.GetExtensions().AsMap()["nested"].(map[string]any)["enabled"]; got != true {
		t.Fatalf("source mutation crossed mapper boundary: %v", got)
	}
	proto.Extensions.Fields["key"] = structpb.NewStringValue("wire-mutated")
	if got := profile.Extensions["key"]; got != "value" {
		t.Fatalf("wire mutation crossed mapper boundary: %v", got)
	}
}

// TestProviderToAdapterConfigProfile verifies that providerToAdapterConfig
// carries the concrete profile into the OpenAICompatAdapterConfig payload.
func TestProtocolProfileProviderToAdapterConfig(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "openai",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver:  config.ProtocolDriverOpenAIChat,
			BaseURL: "https://api.openai.com/v1",
			Operations: map[string]string{
				"models": "/models",
			},
			Auth:         config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
			Capabilities: []string{"chat"},
		},
	}

	p := config.NodeProviderConf{
		ID:             "my-provider",
		Type:           "openai_api",
		Profile:        "openai",
		RuntimeProfile: &profile,
		Endpoint:       "https://api.openai.com/v1",
		Models:         []string{"gpt-4"},
		Capacity:       4,
	}

	ac, err := providerToAdapterConfig(p)
	if err != nil {
		t.Fatalf("providerToAdapterConfig: %v", err)
	}
	if ac.GetType() != "openai_compat" {
		t.Errorf("Type = %q, want %q", ac.GetType(), "openai_compat")
	}
	if ac.GetName() != "my-provider" {
		t.Errorf("Name = %q, want %q", ac.GetName(), "my-provider")
	}
	openaiCfg := ac.GetOpenaiCompat()
	if openaiCfg == nil {
		t.Fatal("expected non-nil OpenAICompatAdapterConfig")
	}
	if openaiCfg.GetProtocolProfile() == nil {
		t.Error("expected non-nil protocol_profile in payload")
	}
	if openaiCfg.GetProtocolProfile().GetId() != "openai" {
		t.Errorf("ProtocolProfile.Id = %q, want %q", openaiCfg.GetProtocolProfile().GetId(), "openai")
	}
}

// TestBuildConfigPayloadNoBaseCrossesWire is a guard that the mapper does not
// leak a Base reference into the wire payload for any adapter type.
func TestProtocolProfileBuildConfigPayloadNoBase(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "openai",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver:  config.ProtocolDriverOpenAIChat,
			BaseURL: "https://api.openai.com/v1",
			Operations: map[string]string{
				"models": "/models",
			},
			Auth:         config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"},
			Capabilities: []string{"chat"},
		},
	}

	rec := &NodeRecord{
		ID: "node-1",
		Providers: []config.NodeProviderConf{
			{
				ID:             "my-provider",
				Type:           "openai_api",
				Profile:        "openai",
				RuntimeProfile: &profile,
				Endpoint:       "https://api.openai.com/v1",
				Models:         []string{"gpt-4"},
				Capacity:       4,
			},
		},
	}

	payload, err := BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("BuildConfigPayload: %v", err)
	}
	if len(payload.GetAdapters()) != 1 {
		t.Fatalf("expected 1 adapter, got %d", len(payload.GetAdapters()))
	}
	openaiCfg := payload.GetAdapters()[0].GetOpenaiCompat()
	if openaiCfg == nil {
		t.Fatal("expected non-nil OpenAICompatAdapterConfig")
	}
	// ConcreteProtocolProfile proto has no Base field, so this is always nil.
	_ = openaiCfg.GetProtocolProfile()
}

func TestProtocolProfileProviderReusingBackingKeyEmitsOneAdapter(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "openai_chat",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver: config.ProtocolDriverOpenAIChat,
		},
	}
	rec := &NodeRecord{
		ID:    "node-profile-reuse",
		Alias: "profile-reuse",
		Token: "token",
		Providers: []config.NodeProviderConf{
			{
				ID:               "provider-a",
				Type:             "vllm",
				Adapter:          "provider-a",
				Capacity:         2,
				RequestTimeoutMS: 120000,
				RuntimeProfile:   &profile,
			},
		},
		Adapters: config.AdaptersConf{
			OpenAICompatInstances: []config.OpenAICompatInstanceConf{
				{
					Name:             "provider-a",
					Enabled:          true,
					Provider:         "vllm",
					Endpoint:         "http://127.0.0.1:8000/v1",
					Capacity:         2,
					RequestTimeoutMS: 120000,
				},
			},
		},
	}

	payload, err := BuildConfigPayload(rec)
	if err != nil {
		t.Fatalf("BuildConfigPayload: %v", err)
	}
	if len(payload.GetAdapters()) != 1 {
		t.Fatalf("adapter count = %d, want 1", len(payload.GetAdapters()))
	}
	adapter := payload.GetAdapters()[0]
	if adapter.GetName() != "provider-a" {
		t.Fatalf("adapter name = %q, want provider-a", adapter.GetName())
	}
	openAICompat := adapter.GetOpenaiCompat()
	if openAICompat == nil {
		t.Fatal("expected OpenAI-compatible adapter")
	}
	if openAICompat.GetEndpoint() != "http://127.0.0.1:8000/v1" {
		t.Fatalf("endpoint = %q", openAICompat.GetEndpoint())
	}
	if openAICompat.GetProtocolProfile() == nil {
		t.Fatal("expected resolved protocol profile")
	}
}

func TestProtocolProfileProviderReusingBackingKeyRejectsOtherLegacyType(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "openai_chat",
		ProtocolProfileConf: config.ProtocolProfileConf{
			Driver: config.ProtocolDriverOpenAIChat,
		},
	}
	rec := &NodeRecord{
		ID:    "node-profile-reuse-conflict",
		Alias: "profile-reuse-conflict",
		Token: "token",
		Providers: []config.NodeProviderConf{
			{
				ID:             "provider-a",
				Type:           "vllm",
				Adapter:        "provider-a",
				Capacity:       2,
				RuntimeProfile: &profile,
			},
		},
		Adapters: config.AdaptersConf{
			OllamaInstances: []config.OllamaInstanceConf{
				{Name: "provider-a", Enabled: true, Capacity: 2},
			},
			OpenAICompatInstances: []config.OpenAICompatInstanceConf{
				{
					Name:     "provider-a",
					Enabled:  true,
					Provider: "vllm",
					Endpoint: "http://127.0.0.1:8000/v1",
					Capacity: 2,
				},
			},
		},
	}

	_, err := BuildConfigPayload(rec)
	if err == nil || !strings.Contains(err.Error(), "ollama adapter instance key") {
		t.Fatalf("BuildConfigPayload error = %v, want ollama key conflict", err)
	}
}

func TestProtocolProfileAdapterBackingCompile(t *testing.T) {
	profile := config.ConcreteProtocolProfile{
		ID: "custom", ProtocolProfileConf: config.ProtocolProfileConf{
			Driver: config.ProtocolDriverOpenAIChat, BaseURL: "http://backing.invalid/v1",
			Operations: map[string]string{"chat_completions": "/chat/completions"},
			Auth:       config.ProtocolAuthConf{Header: "Authorization", Scheme: "Bearer"}, Capabilities: []string{"chat"},
		},
	}
	t.Run("vllm_backing_with_provider_overlay", func(t *testing.T) {
		rec := &NodeRecord{
			ID: "node-vllm",
			Adapters: config.AdaptersConf{VllmInstances: []config.VllmInstanceConf{{
				Name: "gpu", Enabled: true, Endpoint: "http://vllm:8000/v1", Capacity: 2, MaxQueue: 3, QueueTimeoutMS: 4, RequestTimeoutMS: 5,
			}}},
			Providers: []config.NodeProviderConf{{
				ID: "profile-provider", Type: "vllm", Adapter: "gpu", RuntimeProfile: &profile,
				Endpoint: "http://overlay:9000/v1", Capacity: 7,
			}},
		}
		payload, err := BuildConfigPayload(rec)
		if err != nil {
			t.Fatalf("BuildConfigPayload: %v", err)
		}
		if len(payload.GetAdapters()) != 2 {
			t.Fatalf("adapters = %d, want profile adapter plus retained backing", len(payload.GetAdapters()))
		}
		compiled := payload.GetAdapters()[0]
		got := compiled.GetOpenaiCompat()
		if compiled.GetName() != "profile-provider" || got == nil {
			t.Fatalf("compiled adapter = %#v", compiled)
		}
		if got.GetProvider() != "vllm" || got.GetEndpoint() != "http://overlay:9000/v1" || got.GetCapacity() != 7 || got.GetMaxQueue() != 3 {
			t.Fatalf("compiled backing overlay = %#v", got)
		}
		if got.GetProtocolProfile().GetId() != "custom" || payload.GetAdapters()[1].GetVllm() == nil || payload.GetAdapters()[1].GetName() != "gpu" {
			t.Fatalf("profile/backing payload = %#v", payload.GetAdapters())
		}
	})

	t.Run("openai_compat_backing_fields", func(t *testing.T) {
		rec := &NodeRecord{
			ID: "node-openai",
			Adapters: config.AdaptersConf{OpenAICompatInstances: []config.OpenAICompatInstanceConf{{
				Name: "api", Enabled: true, Provider: "lemonade", Endpoint: "http://api:8000/v1",
				Headers: map[string]string{"Authorization": "Bearer secret"}, Capacity: 2, RequestTimeoutMS: 90,
			}}},
			Providers: []config.NodeProviderConf{{ID: "profile-provider", Type: "openai_compat", Adapter: "api", RuntimeProfile: &profile}},
		}
		payload, err := BuildConfigPayload(rec)
		if err != nil {
			t.Fatalf("BuildConfigPayload: %v", err)
		}
		got := payload.GetAdapters()[0].GetOpenaiCompat()
		if got.GetProvider() != "lemonade" || got.GetEndpoint() != "http://api:8000/v1" || got.GetHeaders()["Authorization"] != "Bearer secret" || got.GetRequestTimeoutMs() != 90 {
			t.Fatalf("compiled adapter did not inherit backing fields: %#v", got)
		}
		got.Headers["Authorization"] = "mutated"
		if rec.Adapters.OpenAICompatInstances[0].Headers["Authorization"] != "Bearer secret" {
			t.Fatal("wire headers alias backing config")
		}
	})
}

func TestProtocolProfileBackingAdapterRejected(t *testing.T) {
	profile := config.ConcreteProtocolProfile{ID: "openai", ProtocolProfileConf: config.ProtocolProfileConf{Driver: config.ProtocolDriverOpenAIChat}}
	for _, tc := range []struct {
		name, ref, want string
		adapters        config.AdaptersConf
	}{
		{name: "missing", ref: "missing", want: "missing"},
		{name: "disabled", ref: "api", want: "disabled", adapters: config.AdaptersConf{OpenAICompatInstances: []config.OpenAICompatInstanceConf{{Name: "api", Enabled: false}}}},
		{name: "ambiguous_exact", ref: "shared", want: "ambiguous", adapters: config.AdaptersConf{
			VllmInstances:         []config.VllmInstanceConf{{Name: "shared", Enabled: true}},
			OpenAICompatInstances: []config.OpenAICompatInstanceConf{{Name: "shared", Enabled: true}},
		}},
		{name: "ambiguous_type", ref: "vllm", want: "ambiguous", adapters: config.AdaptersConf{VllmInstances: []config.VllmInstanceConf{
			{Name: "one", Enabled: true}, {Name: "two", Enabled: true},
		}}},
	} {
		t.Run(tc.name, func(t *testing.T) {
			rec := &NodeRecord{ID: "node", Adapters: tc.adapters, Providers: []config.NodeProviderConf{{
				ID: "profile-provider", Type: "openai_compat", Adapter: tc.ref, RuntimeProfile: &profile,
			}}}
			_, err := BuildConfigPayload(rec)
			if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "profile-provider") || !strings.Contains(err.Error(), tc.ref) {
				t.Fatalf("expected contextual %q error, got %v", tc.want, err)
			}
		})
	}
}
