package service

import (
	"testing"

	edgeevents "iop/apps/edge/internal/events"
	edgenode "iop/apps/edge/internal/node"
	"iop/packages/go/config"
)

func TestListNodeSnapshotsUsesEdgeQueueState(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-q-1", Alias: "node-q-1"})

	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-q-1",
		Adapters: config.AdaptersConf{
			OllamaInstances: []config.OllamaInstanceConf{
				{
					Name:     "ollama-inst",
					Enabled:  true,
					Capacity: 3,
				},
			},
		},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	// Manually inject group state to modelQueueManager
	svc.queue.mu.Lock()
	svc.queue.groups["test-group"] = &modelQueueGroup{
		key:      "test-group",
		adapter:  "ollama-inst",
		inflight: map[string]int{"node-q-1": 2},
		queue: []*queueItem{
			{
				candidates: []candidateNode{
					{entry: reg.All()[0], capacity: 3},
				},
			},
		},
	}
	svc.queue.mu.Unlock()

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]
	if len(snap.ProviderSnapshots) != 1 {
		t.Fatalf("expected 1 provider snapshot, got %d", len(snap.ProviderSnapshots))
	}
	p := snap.ProviderSnapshots[0]
	if p.Adapter != "ollama-inst" {
		t.Errorf("expected adapter ollama-inst, got %s", p.Adapter)
	}
	if p.Capacity != 3 {
		t.Errorf("expected capacity 3, got %d", p.Capacity)
	}
	if p.InFlight != 2 {
		t.Errorf("expected inflight 2, got %d", p.InFlight)
	}
	if p.Queued != 1 {
		t.Errorf("expected queued 1, got %d", p.Queued)
	}
}

func TestListNodeSnapshotsTypeRouteSingleNamedInstance(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-q-2", Alias: "node-q-2"})

	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-q-2",
		Adapters: config.AdaptersConf{
			OllamaInstances: []config.OllamaInstanceConf{
				{
					Name:     "ollama-local",
					Enabled:  true,
					Capacity: 5,
				},
			},
		},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	// Manually inject group state to modelQueueManager with type-name route "ollama"
	svc.queue.mu.Lock()
	svc.queue.groups["test-group"] = &modelQueueGroup{
		key:      "test-group",
		adapter:  "ollama",
		inflight: map[string]int{"node-q-2": 3},
		queue: []*queueItem{
			{
				candidates: []candidateNode{
					{entry: reg.All()[0], capacity: 5},
				},
			},
		},
	}
	svc.queue.mu.Unlock()

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]
	if len(snap.ProviderSnapshots) != 1 {
		t.Fatalf("expected 1 provider snapshot, got %d", len(snap.ProviderSnapshots))
	}
	p := snap.ProviderSnapshots[0]
	if p.Adapter != "ollama-local" {
		t.Errorf("expected adapter ollama-local, got %s", p.Adapter)
	}
	if p.Capacity != 5 {
		t.Errorf("expected capacity 5, got %d", p.Capacity)
	}
	if p.InFlight != 3 {
		t.Errorf("expected inflight 3, got %d", p.InFlight)
	}
	if p.Queued != 1 {
		t.Errorf("expected queued 1, got %d", p.Queued)
	}
}

// REVIEW_API-2: provider id와 adapter key가 다른 경우에도 queue stats가 올바르게 반영되는지 검증.
// provider id는 "provider-vllm-primary"지만, adapter/instance key는 "vllm-gpu"이다.
// queue group에 adapter="vllm-gpu"로 inject하면 provider snapshot이 이 stats를 올바르게 반영해야 한다.
// REVIEW_REVIEW_API-2: adapter가 비어 있을 때 provider id로 fallback하여
// queue stats(InFlight, Queued, LoadRatio)가 올바르게 반영되는지 검증.
func TestListNodeSnapshotsProviderEmptyAdapterFallbackToProviderID(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-e-1", Alias: "node-e-1"})

	// Node has a provider with empty adapter. Queue state is keyed by provider id.
	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID:       "node-e-1",
		Adapters: config.AdaptersConf{
			// No OllamaInstances/vllm_instances matching the provider id.
			// This ensures the fallback path is tested.
		},
		Providers: []config.NodeProviderConf{
			{
				ID:       "standalone-provider",
				Type:     "cli",
				Category: config.CategoryCLI,
				Adapter:  "", // empty — should fallback to provider id
				Models:   []string{"test-model"},
				Capacity: 2,
			},
		},
		Runtime: config.RuntimeConf{Concurrency: 2},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	// Inject tracking state the way provider-pool dispatch does it:
	// inflightByRun keyed by providerID, and queue candidates with providerID set.
	svc.queue.mu.Lock()
	svc.queue.inflightByRun["run-sp-001"] = inflightRec{
		groupKey:   "test-group",
		nodeID:     "node-e-1",
		providerID: "standalone-provider",
	}
	svc.queue.groups["test-group"] = &modelQueueGroup{
		key:      "test-group",
		adapter:  "standalone-provider",
		inflight: map[string]int{"node-e-1": 1},
		queue: []*queueItem{
			{
				candidates: []candidateNode{
					{entry: reg.All()[0], capacity: 2, providerID: "standalone-provider"},
				},
			},
		},
	}
	svc.queue.mu.Unlock()

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]

	// Find the provider snapshot by its provider id.
	found := false
	for i, ps := range snap.ProviderSnapshots {
		if ps.Id == "standalone-provider" {
			found = true
			if ps.InFlight != 1 {
				t.Errorf("expected InFlight=1 for standalone-provider (adapter empty), got %d", ps.InFlight)
			}
			if ps.Queued != 1 {
				t.Errorf("expected Queued=1 for standalone-provider (adapter empty), got %d", ps.Queued)
			}
			if ps.Capacity != 2 {
				t.Errorf("expected Capacity=2 for standalone-provider, got %d", ps.Capacity)
			}
			// LoadRatio = InFlight / Capacity = 1 / 2 = 0.5
			expectedLoadRatio := float32(1) / float32(2)
			if ps.LoadRatio != expectedLoadRatio {
				t.Errorf("expected LoadRatio=%.2f for standalone-provider, got %.2f", expectedLoadRatio, ps.LoadRatio)
			}
			t.Logf("provider snapshot[%d]: id=%s, adapter=%s, InFlight=%d, Queued=%d, Capacity=%d, LoadRatio=%.2f",
				i, ps.Id, ps.Adapter, ps.InFlight, ps.Queued, ps.Capacity, ps.LoadRatio)
			break
		}
	}
	if !found {
		t.Fatalf("expected provider snapshot with id=standalone-provider, got %d provider snapshots", len(snap.ProviderSnapshots))
	}
}

// TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots verifies the
// catalog-first rule: when a node has both adapter instances (CLI + OpenAICompat)
// and a providers[] catalog, ListNodeSnapshots returns only the catalog snapshots
// with no adapter duplicates.
func TestListNodeSnapshotsProviderCatalogSupersedesAdapterSnapshots(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-cs-1", Alias: "node-cs-1"})

	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-cs-1",
		Adapters: config.AdaptersConf{
			CLI: config.CLIConf{Enabled: true},
			OpenAICompatInstances: []config.OpenAICompatInstanceConf{
				{Name: "vllm-gpu", Enabled: true},
			},
		},
		Providers: []config.NodeProviderConf{
			{ID: "prov-cat-1", Adapter: "vllm-gpu", Models: []string{"model-x"}, Health: "available", Capacity: 4},
			{ID: "prov-cat-2", Adapter: "vllm-gpu", Models: []string{"model-y"}, Health: "available", Capacity: 2},
		},
		Runtime: config.RuntimeConf{Concurrency: 1},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]

	// Only catalog snapshots must be returned — exactly 2 (one per provider).
	if len(snap.ProviderSnapshots) != 2 {
		t.Fatalf("expected 2 catalog snapshots, got %d (cli/openai_compat adapter duplicates must be absent): %+v",
			len(snap.ProviderSnapshots), snap.ProviderSnapshots)
	}
	ids := map[string]bool{}
	for _, p := range snap.ProviderSnapshots {
		if p.Id == "" {
			t.Errorf("unexpected adapter-only snapshot (empty Id): %+v", p)
		}
		ids[p.Id] = true
	}
	if !ids["prov-cat-1"] || !ids["prov-cat-2"] {
		t.Errorf("expected catalog IDs prov-cat-1 and prov-cat-2, got: %v", ids)
	}
}

func TestListNodeSnapshotsCatalogProviderZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-zero-cap", Alias: "node-zero-cap"})

	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-zero-cap",
		Adapters: config.AdaptersConf{
			OpenAICompatInstances: []config.OpenAICompatInstanceConf{
				{Name: "vllm-gpu", Enabled: true, Capacity: 7},
			},
		},
		Providers: []config.NodeProviderConf{
			{ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0},
		},
		Runtime: config.RuntimeConf{Concurrency: 7},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	if len(snaps[0].ProviderSnapshots) != 1 {
		t.Fatalf("expected 1 provider snapshot, got %d", len(snaps[0].ProviderSnapshots))
	}
	p := snaps[0].ProviderSnapshots[0]
	if p.Id != "prov-zero" {
		t.Fatalf("expected provider id prov-zero, got %q", p.Id)
	}
	if p.Capacity != 0 {
		t.Fatalf("provider catalog capacity must not fall back to runtime concurrency: got %d", p.Capacity)
	}
	if p.LoadRatio != 0 {
		t.Fatalf("expected zero load ratio for zero capacity provider, got %.2f", p.LoadRatio)
	}
}

func TestStaticProviderCatalogSnapshotsZeroCapacityDoesNotUseRuntimeFallback(t *testing.T) {
	rec := &edgenode.NodeRecord{
		ID: "node-static-zero-cap",
		Providers: []config.NodeProviderConf{
			{ID: "prov-zero", Adapter: "vllm-gpu", Models: []string{"model-zero"}, Health: "available", Capacity: 0},
		},
		Runtime: config.RuntimeConf{Concurrency: 7},
	}

	snaps := staticProviderCatalogSnapshots(rec)
	if len(snaps) != 1 {
		t.Fatalf("expected 1 provider snapshot, got %d", len(snaps))
	}
	if snaps[0].Capacity != 0 {
		t.Fatalf("static provider catalog capacity must not fall back to runtime concurrency: got %d", snaps[0].Capacity)
	}
}

// TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders verifies that when
// a node has no providers[] catalog, ListNodeSnapshots returns the legacy adapter
// snapshots (CLI, Ollama, etc.) rather than empty results.
func TestListNodeSnapshotsLegacyAdapterFallbackWhenNoProviders(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-legacy-snap", Alias: "node-legacy-snap"})

	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-legacy-snap",
		Adapters: config.AdaptersConf{
			CLI: config.CLIConf{Enabled: true},
			OllamaInstances: []config.OllamaInstanceConf{
				{Name: "ollama-local", Enabled: true, Capacity: 3},
			},
		},
		Runtime: config.RuntimeConf{Concurrency: 2},
		// Providers is empty → legacy adapter fallback
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]

	// Must return CLI + ollama-local adapter snapshots.
	if len(snap.ProviderSnapshots) != 2 {
		t.Fatalf("expected 2 adapter snapshots (cli + ollama-local), got %d: %+v",
			len(snap.ProviderSnapshots), snap.ProviderSnapshots)
	}
	adapters := map[string]bool{}
	for _, p := range snap.ProviderSnapshots {
		adapters[p.Adapter] = true
	}
	if !adapters["cli"] || !adapters["ollama-local"] {
		t.Errorf("expected cli and ollama-local adapter snapshots, got: %v", adapters)
	}
}

func TestListNodeSnapshotsProviderIdDiffersFromAdapterKey(t *testing.T) {
	reg := edgenode.NewRegistry()
	reg.Register(&edgenode.NodeEntry{NodeID: "node-p-1", Alias: "node-p-1"})

	// Node has providers[] with provider id "provider-vllm-primary" and adapter="vllm-gpu".
	// The adapter field points to an existing OllamaInstance named "vllm-gpu" (used here as a stand-in for the adapter key).
	store := edgenode.NewNodeStore()
	rec := &edgenode.NodeRecord{
		ID: "node-p-1",
		Adapters: config.AdaptersConf{
			OllamaInstances: []config.OllamaInstanceConf{
				{
					Name:     "vllm-gpu",
					Enabled:  true,
					Capacity: 4,
				},
			},
		},
		Providers: []config.NodeProviderConf{
			{
				ID:       "provider-vllm-primary",
				Type:     "vllm",
				Category: config.CategoryAPI,
				Adapter:  "vllm-gpu", // adapter key differs from provider id
				Models:   []string{"nvidia/Qwen3.6-35B"},
				Capacity: 4,
			},
		},
		Runtime: config.RuntimeConf{Concurrency: 2},
	}
	store.Add(rec)

	bus := edgeevents.NewBus()
	svc := New(reg, bus)
	svc.SetNodeStore(store)

	// Inject tracking state the way provider-pool dispatch does it:
	// inflightByRun entries keyed by providerID, and queue candidates with providerID set.
	svc.queue.mu.Lock()
	svc.queue.inflightByRun["run-vp-001"] = inflightRec{
		groupKey:   "test-group",
		nodeID:     "node-p-1",
		providerID: "provider-vllm-primary",
	}
	svc.queue.inflightByRun["run-vp-002"] = inflightRec{
		groupKey:   "test-group",
		nodeID:     "node-p-1",
		providerID: "provider-vllm-primary",
	}
	svc.queue.groups["test-group"] = &modelQueueGroup{
		key:      "test-group",
		adapter:  "vllm-gpu",
		inflight: map[string]int{"node-p-1": 2},
		queue: []*queueItem{
			{
				candidates: []candidateNode{
					{entry: reg.All()[0], capacity: 4, providerID: "provider-vllm-primary"},
				},
			},
		},
	}
	svc.queue.mu.Unlock()

	snaps := svc.ListNodeSnapshots()
	if len(snaps) != 1 {
		t.Fatalf("expected 1 node snapshot, got %d", len(snaps))
	}
	snap := snaps[0]

	// Find the provider snapshot by its provider id.
	var p *config.NodeProviderConf
	for i := range snap.ProviderSnapshots {
		ps := snap.ProviderSnapshots[i]
		if ps.Id == "provider-vllm-primary" {
			p = &config.NodeProviderConf{
				Capacity: int(ps.Capacity),
			}
			// Verify queue stats match the injected queue state.
			if ps.InFlight != 2 {
				t.Errorf("expected InFlight=2 for provider-vllm-primary, got %d", ps.InFlight)
			}
			if ps.Queued != 1 {
				t.Errorf("expected Queued=1 for provider-vllm-primary, got %d", ps.Queued)
			}
			if ps.Adapter != "vllm-gpu" {
				t.Errorf("expected Adapter=vllm-gpu for provider-vllm-primary, got %s", ps.Adapter)
			}
			break
		}
	}
	if p == nil {
		t.Fatalf("expected provider snapshot with id=provider-vllm-primary, got %d provider snapshots", len(snap.ProviderSnapshots))
	}
}
