package bootstrap

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"testing"

	"iop/packages/go/config"
)

func newTestConfig() *config.EdgeConfig {
	return &config.EdgeConfig{
		Server:  config.EdgeServerConf{Listen: "127.0.0.1:0"},
		Logging: config.LoggingConf{Level: "error"},
		Metrics: config.MetricsConf{Port: 0},
		Nodes: []config.NodeDefinition{
			{ID: "node-1", Alias: "alias-1", Token: "tok-1"},
		},
	}
}

func serveNormalizedLogPath() string {
	if exe, err := os.Executable(); err == nil {
		return filepath.Join(filepath.Dir(exe), "logs", "edge.log")
	}
	if wd, err := os.Getwd(); err == nil {
		return filepath.Join(wd, "logs", "edge.log")
	}
	return "logs/edge.log"
}

func serveNormalizedArtifactDir(dir string) string {
	if dir == "" {
		dir = "artifacts"
	}
	if filepath.IsAbs(dir) {
		return dir
	}
	bd := binaryDirForTest()
	return filepath.Join(bd, dir)
}

func binaryDirForTest() string {
	if exe, err := os.Executable(); err == nil {
		return filepath.Dir(exe)
	}
	if wd, err := os.Getwd(); err == nil {
		return wd
	}
	return "."
}

// loadServeNormalizedConfig loads an Edge config and applies the same
// normalization iop-edge serve performs, so refresh classification compares
// effective values.
func loadServeNormalizedConfig(t *testing.T, path string) *config.EdgeConfig {
	t.Helper()
	cfg, err := config.LoadEdge(path)
	if err != nil {
		t.Fatalf("load config %s: %v", path, err)
	}
	cfg.Logging.Path = serveNormalizedLogPath()
	cfg.Bootstrap.ArtifactDir = serveNormalizedArtifactDir(cfg.Bootstrap.ArtifactDir)
	return cfg
}

// providerPoolConfigYAML builds an Edge config with one provider-pool model and
// one node provider. The extra parameter lets callers inject a candidate
// variation (e.g. a different capacity or an invalid adapter block).
func providerPoolConfigYAML(serverAddr, openAIAddr, providerBlock string) string {
	return fmt.Sprintf(`
server:
  listen: %q
bootstrap:
  listen: "0.0.0.0:18080"
  artifact_dir: "artifacts"
logging:
  level: "error"
refresh:
  enabled: true
  listen: "127.0.0.1:0"
openai:
  enabled: true
  listen: %q
  provider_id: "test-provider"
  adapter: "openai_compat"
  target: ""
a2a:
  listen: "0.0.0.0:8081"
metrics:
  port: 0
models:
  - id: "qwen3.6:35b"
    display_name: "Qwen"
    providers:
      prov-a: "served-qwen"
nodes:
  - id: "node-1"
    alias: "n1"
    token: "tok-1"
    adapters:
%s
    providers:
      - id: "prov-a"
        type: "vllm"
        category: "api"
        adapter: "vllm-gpu"
        models: ["served-qwen"]
        health: "available"
        capacity: 2
        max_queue: 4
        queue_timeout_ms: 5000
`, serverAddr, openAIAddr, providerBlock)
}

const validAdapterBlock = `      openai_compat_instances:
        - name: "vllm-gpu"
          enabled: true
          provider: "vllm"
          endpoint: "http://127.0.0.1:8000/v1"`

// invalidAdapterBlock enables an openai_compat instance but leaves endpoint
// empty, which ValidateEdgeConfig rejects.
const invalidAdapterBlock = `      openai_compat_instances:
        - name: "vllm-gpu"
          enabled: true
          provider: "vllm"
          endpoint: ""`

// modelsResponse represents the JSON response from /v1/models.
type modelsResponse struct {
	Object string      `json:"object"`
	Data   []modelItem `json:"data"`
}

// modelItem represents a single model entry in the /v1/models response.
type modelItem struct {
	ID      string `json:"id"`
	Object  string `json:"object"`
	Created int64  `json:"created"`
	OwnedBy string `json:"owned_by"`
}

// modelIDs returns the sorted list of model IDs from /v1/models.
// This avoids flaky timestamp comparisons on the `created` field.
func modelIDs(t *testing.T, client *http.Client, openAIAddr string) []string {
	t.Helper()
	resp, err := client.Get("http://" + openAIAddr + "/v1/models")
	if err != nil {
		t.Fatalf("GET /v1/models: %v", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		t.Fatalf("GET /v1/models status=%d", resp.StatusCode)
	}
	var body modelsResponse
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		t.Fatalf("decode /v1/models body: %v", err)
	}
	ids := make([]string, 0, len(body.Data))
	for _, m := range body.Data {
		ids = append(ids, m.ID)
	}
	sort.Strings(ids)
	return ids
}

func containsString(s []string, want string) bool {
	for _, v := range s {
		if v == want {
			return true
		}
	}
	return false
}

func freeTCPAddr(t *testing.T) string {
	t.Helper()
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("listen free tcp addr: %v", err)
	}
	addr := ln.Addr().String()
	if err := ln.Close(); err != nil {
		t.Fatalf("close free tcp addr listener: %v", err)
	}
	return addr
}
