package adapters

import (
	"reflect"
	"strings"
	"testing"

	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/structpb"

	"iop/packages/go/config"
	iop "iop/proto/gen/iop"
)

func TestProtocolProfileExtensionsRoundTrip(t *testing.T) {
	profile := mustResolveProfileForAdapter(t, "openai")
	profile.Extensions = map[string]any{
		"nested": map[string]any{
			"enabled": true,
			"items":   []any{"one", map[string]any{"leaf": "two"}},
		},
	}
	wire := concreteProfileToProtoForTest(&profile)
	decoded := concreteProfileFromProto(wire)
	if decoded == nil {
		t.Fatal("decoded profile is nil")
	}
	if !reflect.DeepEqual(decoded.Extensions, profile.Extensions) {
		t.Fatalf("extensions = %#v, want %#v", decoded.Extensions, profile.Extensions)
	}
	profile.Extensions["nested"].(map[string]any)["enabled"] = false
	if got := decoded.Extensions["nested"].(map[string]any)["enabled"]; got != true {
		t.Fatalf("source mutation crossed Node boundary: %v", got)
	}
	decoded.Extensions["nested"].(map[string]any)["items"].([]any)[1].(map[string]any)["leaf"] = "changed"
	if got := wire.GetExtensions().AsMap()["nested"].(map[string]any)["items"].([]any)[1].(map[string]any)["leaf"]; got != "two" {
		t.Fatalf("decoded mutation crossed wire boundary: %v", got)
	}
	payload := func(profile *iop.ConcreteProtocolProfile) *iop.NodeConfigPayload {
		return &iop.NodeConfigPayload{Adapters: []*iop.AdapterConfig{{
			Type: "openai_compat", Enabled: true, Name: "ext-adapter",
			Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{ProtocolProfile: profile}},
		}}}
	}
	first, err := BuildConfigSet(payload(wire), zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet first: %v", err)
	}
	wireChanged := concreteProfileToProtoForTest(&profile)
	second, err := BuildConfigSet(payload(wireChanged), zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet second: %v", err)
	}
	if first.Items["ext-adapter"].Fingerprint == second.Items["ext-adapter"].Fingerprint {
		t.Fatal("nested extension change did not change config fingerprint")
	}
}

func TestProtocolProfileAdapterBackedConfigFingerprint(t *testing.T) {
	profile := mustResolveProfileForAdapter(t, "openai")
	profile.ModelMapping = map[string]string{"canonical": "upstream-a"}
	payload := func(p config.ConcreteProtocolProfile) *iop.NodeConfigPayload {
		return &iop.NodeConfigPayload{Adapters: []*iop.AdapterConfig{
			{
				Type: "openai_compat", Enabled: true, Name: "profile-provider",
				Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{
					Provider: "lemonade", Endpoint: "http://backing:8000/v1",
					Headers: map[string]string{"Authorization": "Bearer secret"}, ProtocolProfile: concreteProfileToProtoForTest(&p),
				}},
			},
			{
				Type: "openai_compat", Enabled: true, Name: "legacy-backing",
				Config: &iop.AdapterConfig_OpenaiCompat{OpenaiCompat: &iop.OpenAICompatAdapterConfig{
					Provider: "lemonade", Endpoint: "http://backing:8000/v1", Headers: map[string]string{"Authorization": "Bearer secret"},
				}},
			},
		}}
	}
	first, err := BuildConfigSet(payload(profile), zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet first: %v", err)
	}
	if len(first.Items) != 2 {
		t.Fatalf("items = %d, want profile provider plus retained backing", len(first.Items))
	}
	fingerprint := first.Items["profile-provider"].Fingerprint
	for _, fragment := range []string{"http://backing:8000/v1", "Authorization", "upstream-a"} {
		if !strings.Contains(fingerprint, fragment) {
			t.Errorf("profile fingerprint does not contain %q: %s", fragment, fingerprint)
		}
	}
	profile.ModelMapping["canonical"] = "upstream-b"
	second, err := BuildConfigSet(payload(profile), zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet second: %v", err)
	}
	diff := DiffConfigSets(first, second)
	if !reflect.DeepEqual(diff.Updated, []string{"profile-provider"}) {
		t.Fatalf("profile-only diff = %+v", diff)
	}
	if first.Items["legacy-backing"].Fingerprint != second.Items["legacy-backing"].Fingerprint {
		t.Fatal("profile change altered retained legacy backing fingerprint")
	}
}

// TestProtocolProfileConfigUpdate verifies that a profile change in the config
// payload produces a different fingerprint, triggering a config update.
func TestProtocolProfileConfigUpdate(t *testing.T) {
	profile := mustResolveProfileForAdapter(t, "openai")

	payload1 := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "my-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Provider:        "openai_api",
						Endpoint:        "https://api.openai.com/v1",
						Capacity:        4,
						ProtocolProfile: concreteProfileToProtoForTest(&profile),
					},
				},
			},
		},
	}

	set1, err := BuildConfigSet(payload1, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 1: %v", err)
	}

	// Change the profile to anthropic.
	anthropicProfile := mustResolveProfileForAdapter(t, "anthropic")
	payload2 := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "my-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Provider:        "openai_api",
						Endpoint:        "https://api.openai.com/v1",
						Capacity:        4,
						ProtocolProfile: concreteProfileToProtoForTest(&anthropicProfile),
					},
				},
			},
		},
	}

	set2, err := BuildConfigSet(payload2, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 2: %v", err)
	}

	diff := DiffConfigSets(set1, set2)
	if len(diff.Updated) != 1 || diff.Updated[0] != "my-adapter" {
		t.Fatalf("expected my-adapter to be updated, got diff: %+v", diff)
	}
}

// TestProtocolProfileConfigInheritedStateAbsent verifies that the concrete
// profile snapshot does not carry inherited Base state into the adapter config.
func TestProtocolProfileConfigInheritedStateAbsent(t *testing.T) {
	profile := mustResolveProfileForAdapter(t, "openai")

	payload := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "my-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Provider:        "openai_api",
						Endpoint:        "https://api.openai.com/v1",
						ProtocolProfile: concreteProfileToProtoForTest(&profile),
					},
				},
			},
		},
	}

	set, err := BuildConfigSet(payload, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet: %v", err)
	}

	item, ok := set.Items["my-adapter"]
	if !ok {
		t.Fatal("expected my-adapter in config set")
	}
	if item.Type != "openai_compat" {
		t.Errorf("Type = %q, want %q", item.Type, "openai_compat")
	}
	if item.Fingerprint == "" {
		t.Error("expected non-empty fingerprint")
	}
}

// TestProtocolProfileConfigNilProfile verifies that a payload without a profile
// (legacy) still builds a config set with a valid fingerprint.
func TestProtocolProfileConfigNilProfile(t *testing.T) {
	payload := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "legacy-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Provider: "openai_api",
						Endpoint: "https://api.openai.com/v1",
						Capacity: 4,
						// ProtocolProfile is nil (legacy payload)
					},
				},
			},
		},
	}

	set, err := BuildConfigSet(payload, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet: %v", err)
	}

	item, ok := set.Items["legacy-adapter"]
	if !ok {
		t.Fatal("expected legacy-adapter in config set")
	}
	if item.Fingerprint == "" {
		t.Error("expected non-empty fingerprint for legacy config")
	}
}

// TestProtocolProfileConfigFingerprintIncludesProfile verifies that the
// fingerprint includes the profile data so that profile changes are detected.
func TestProtocolProfileConfigFingerprintIncludesProfile(t *testing.T) {
	profile1 := mustResolveProfileForAdapter(t, "openai")
	profile2 := mustResolveProfileForAdapter(t, "anthropic")

	payload1 := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "fp-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Endpoint:        "https://api.openai.com/v1",
						ProtocolProfile: concreteProfileToProtoForTest(&profile1),
					},
				},
			},
		},
	}
	payload2 := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "fp-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Endpoint:        "https://api.openai.com/v1",
						ProtocolProfile: concreteProfileToProtoForTest(&profile2),
					},
				},
			},
		},
	}

	set1, err := BuildConfigSet(payload1, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 1: %v", err)
	}
	set2, err := BuildConfigSet(payload2, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 2: %v", err)
	}

	fp1 := set1.Items["fp-adapter"].Fingerprint
	fp2 := set2.Items["fp-adapter"].Fingerprint
	if fp1 == fp2 {
		t.Error("expected different fingerprints for different profiles")
	}
}

// TestProtocolProfileConfigDiffUnchanged verifies that identical profile
// payloads produce no diff.
func TestProtocolProfileConfigDiffUnchanged(t *testing.T) {
	profile := mustResolveProfileForAdapter(t, "openai")

	payload := &iop.NodeConfigPayload{
		Adapters: []*iop.AdapterConfig{
			{
				Type:    "openai_compat",
				Enabled: true,
				Name:    "same-adapter",
				Config: &iop.AdapterConfig_OpenaiCompat{
					OpenaiCompat: &iop.OpenAICompatAdapterConfig{
						Endpoint:        "https://api.openai.com/v1",
						ProtocolProfile: concreteProfileToProtoForTest(&profile),
					},
				},
			},
		},
	}

	set1, err := BuildConfigSet(payload, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 1: %v", err)
	}
	set2, err := BuildConfigSet(payload, zap.NewNop())
	if err != nil {
		t.Fatalf("BuildConfigSet 2: %v", err)
	}

	diff := DiffConfigSets(set1, set2)
	if len(diff.Updated) != 0 {
		t.Errorf("expected no updated items, got %v", diff.Updated)
	}
	if len(diff.Added) != 0 {
		t.Errorf("expected no added items, got %v", diff.Added)
	}
}

// concreteProfileToProtoForTest converts a config.ConcreteProtocolProfile to
// its wire representation for test payloads.
func concreteProfileToProtoForTest(p *config.ConcreteProtocolProfile) *iop.ConcreteProtocolProfile {
	if p == nil {
		return nil
	}
	cp := p.Clone()
	proto := &iop.ConcreteProtocolProfile{
		Id:           cp.ID,
		Driver:       string(cp.Driver),
		BaseUrl:      cp.BaseURL,
		Auth:         &iop.ProtocolAuth{Header: cp.Auth.Header, Scheme: cp.Auth.Scheme},
		Capabilities: append([]string(nil), cp.Capabilities...),
	}
	if len(cp.Operations) > 0 {
		proto.Operations = make(map[string]string, len(cp.Operations))
		for k, v := range cp.Operations {
			proto.Operations[k] = v
		}
	}
	if len(cp.ModelMapping) > 0 {
		proto.ModelMapping = make(map[string]string, len(cp.ModelMapping))
		for k, v := range cp.ModelMapping {
			proto.ModelMapping[k] = v
		}
	}
	if len(cp.Extensions) > 0 {
		if s, err := structpb.NewStruct(cp.Extensions); err == nil {
			proto.Extensions = s
		}
	}
	return proto
}

// mustResolveProfileForAdapter resolves a built-in profile for use in adapter
// config tests.
func mustResolveProfileForAdapter(t *testing.T, id string) config.ConcreteProtocolProfile {
	t.Helper()
	profile, err := config.ResolveProtocolProfile(id, "", config.BuiltInProtocolProfiles)
	if err != nil {
		t.Fatalf("ResolveProtocolProfile(%q): %v", id, err)
	}
	return profile
}
