package pack

import (
	"crypto/sha256"
	"encoding/hex"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestLoadDirParsesAndValidatesPack(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json": `{
  "pack_id": "openai-cn-pack",
  "version": "1.0.0",
  "vendor": "YourTeam",
  "target_host": "sub2api",
  "min_host_version": "0.1.126",
  "max_host_version": "0.2.x",
  "providers_dir": "providers",
  "checksum_file": "checksums.txt"
}`,
		"providers/deepseek.json": `{
  "provider_id": "deepseek",
  "display_name": "DeepSeek OpenAI Compatible",
  "base_url": "https://api.deepseek.com",
  "platform": "openai",
  "account_type": "apikey",
  "default_models": ["deepseek-chat", "deepseek-reasoner"],
  "smoke_test_model": "deepseek-chat",
  "group_template": {"name": "DeepSeek 默认分组", "rate_multiplier": 1.0},
  "channel_template": {"name": "DeepSeek 默认渠道", "model_mapping": {"deepseek-chat": "deepseek-chat", "deepseek-reasoner": "deepseek-reasoner"}},
  "plan_template": {"name": "DeepSeek 默认套餐", "price": 19.9, "validity_days": 30, "validity_unit": "day"},
  "import": {"supports_multi_key": true, "supports_strict": true, "supports_partial": true}
}`,
	})

	loaded, err := LoadDir(packDir)
	if err != nil {
		t.Fatalf("LoadDir() error = %v", err)
	}

	if loaded.Manifest.PackID != "openai-cn-pack" {
		t.Fatalf("PackID = %q, want %q", loaded.Manifest.PackID, "openai-cn-pack")
	}
	if len(loaded.Providers) != 1 {
		t.Fatalf("len(Providers) = %d, want 1", len(loaded.Providers))
	}
	if loaded.Providers[0].ProviderID != "deepseek" {
		t.Fatalf("ProviderID = %q, want %q", loaded.Providers[0].ProviderID, "deepseek")
	}
}

func TestLoadDirRejectsChecksumMismatch(t *testing.T) {
	packDir := t.TempDir()
	mustWrite(t, filepath.Join(packDir, "pack.json"), `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`)
	mustWrite(t, filepath.Join(packDir, "providers", "deepseek.json"), `{"provider_id":"deepseek","display_name":"DeepSeek","base_url":"https://api.deepseek.com","platform":"openai","account_type":"api","default_models":["deepseek-chat"],"smoke_test_model":"deepseek-chat","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"deepseek-chat":"deepseek-chat"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`)
	mustWrite(t, filepath.Join(packDir, "checksums.txt"), "deadbeef  pack.json\ndeadbeef  providers/deepseek.json\n")

	_, err := LoadDir(packDir)
	if err == nil {
		t.Fatal("LoadDir() error = nil, want checksum mismatch")
	}
	if !strings.Contains(err.Error(), "checksum mismatch") {
		t.Fatalf("LoadDir() error = %v, want checksum mismatch", err)
	}
}

func TestLoadDirRejectsInvalidProviderSchema(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json":               `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/deepseek.json": `{"provider_id":"deepseek","display_name":"DeepSeek","base_url":"http://insecure.example.com","platform":"openai","account_type":"api","default_models":["deepseek-chat"],"smoke_test_model":"missing-model","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"deepseek-chat":"deepseek-chat"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
	})

	_, err := LoadDir(packDir)
	if err == nil {
		t.Fatal("LoadDir() error = nil, want schema validation failure")
	}
	if !strings.Contains(err.Error(), "https") && !strings.Contains(err.Error(), "smoke_test_model") {
		t.Fatalf("LoadDir() error = %v, want schema validation detail", err)
	}
}

func TestLoadDirAllowsInsecureProviderBaseURLWhenExplicitlyEnabled(t *testing.T) {
	t.Setenv(envAllowInsecureProviderBaseURL, "1")

	packDir := createPackFixture(t, map[string]string{
		"pack.json":               `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/deepseek.json": `{"provider_id":"deepseek","display_name":"DeepSeek","base_url":"http://insecure.example.com","platform":"openai","account_type":"apikey","default_models":["deepseek-v4-pro"],"smoke_test_model":"deepseek-v4-pro","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"deepseek-v4-pro":"deepseek-v4-pro"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
	})

	loaded, err := LoadDir(packDir)
	if err != nil {
		t.Fatalf("LoadDir() error = %v, want insecure http provider accepted when explicitly enabled", err)
	}
	if len(loaded.Providers) != 1 || loaded.Providers[0].BaseURL != "http://insecure.example.com" {
		t.Fatalf("LoadDir() providers = %+v, want insecure provider retained", loaded.Providers)
	}
}

func TestLoadDirRejectsSmokeTestModelMissingFromChannelMapping(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json":               `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/deepseek.json": `{"provider_id":"deepseek","display_name":"DeepSeek","base_url":"https://api.deepseek.com","platform":"openai","account_type":"apikey","default_models":["deepseek-v4-pro","deepseek-v4-flash"],"smoke_test_model":"deepseek-v4-pro","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"deepseek-chat":"deepseek-chat","deepseek-reasoner":"deepseek-reasoner"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
	})

	_, err := LoadDir(packDir)
	if err == nil {
		t.Fatal("LoadDir() error = nil, want smoke_test_model channel mapping validation failure")
	}
	if !strings.Contains(err.Error(), "channel_template.model_mapping") || !strings.Contains(err.Error(), "smoke_test_model") {
		t.Fatalf("LoadDir() error = %v, want smoke_test_model channel mapping detail", err)
	}
}

func TestLoadDirRejectsDefaultModelsMissingFromChannelMapping(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json":              `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/minimax.json": `{"provider_id":"minimax","display_name":"MiniMax","base_url":"https://api.minimax.example.com","platform":"openai","account_type":"apikey","default_models":["MiniMax-M2.5-highspeed","MiniMax-M2.7-highspeed"],"smoke_test_model":"MiniMax-M2.7-highspeed","group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"MiniMax-M2.7-highspeed":"MiniMax-M2.7-highspeed"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
	})

	_, err := LoadDir(packDir)
	if err == nil {
		t.Fatal("LoadDir() error = nil, want default_models channel mapping validation failure")
	}
	if !strings.Contains(err.Error(), "default_models") || !strings.Contains(err.Error(), "channel_template.model_mapping") {
		t.Fatalf("LoadDir() error = %v, want default_models channel mapping detail", err)
	}
}

func TestLoadDirParsesProviderHostOverlays(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json":                             `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/kimi-a7m.json":               `{"provider_id":"kimi-a7m","display_name":"Kimi A7M","base_url":"https://kimi.a7m.com.cn/v1","platform":"openai","account_type":"apikey","default_models":["kimi-k2.6"],"smoke_test_model":"kimi-k2.6","host_overlays":[{"overlay_id":"sub2api-stock-v0129-kimi-a7m","display_name":"sub2api stock v0.1.129 Kimi A7M overlay","target_host":"sub2api","min_host_version":"0.1.129","max_host_version":"0.1.129","apply_mode":"manual_overlay","patch_path":"overlays/kimi-a7m-sub2api-v0.1.129.md","notes_path":"overlays/kimi-a7m-sub2api-v0.1.129.md","reason":"stock host still routes chat traffic into unsupported Responses path"}],"group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"kimi-k2.6":"kimi-k2.6"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
		"overlays/kimi-a7m-sub2api-v0.1.129.md": "# overlay\n",
	})

	loaded, err := LoadDir(packDir)
	if err != nil {
		t.Fatalf("LoadDir() error = %v", err)
	}
	if len(loaded.Providers) != 1 || len(loaded.Providers[0].HostOverlays) != 1 {
		t.Fatalf("loaded provider overlays = %+v, want one overlay", loaded.Providers)
	}
}

func TestLoadDirRejectsMissingHostOverlayFile(t *testing.T) {
	packDir := createPackFixture(t, map[string]string{
		"pack.json":               `{"pack_id":"openai-cn-pack","version":"1.0.0","vendor":"x","target_host":"sub2api","min_host_version":"0.1.126","max_host_version":"0.2.x","providers_dir":"providers","checksum_file":"checksums.txt"}`,
		"providers/kimi-a7m.json": `{"provider_id":"kimi-a7m","display_name":"Kimi A7M","base_url":"https://kimi.a7m.com.cn/v1","platform":"openai","account_type":"apikey","default_models":["kimi-k2.6"],"smoke_test_model":"kimi-k2.6","host_overlays":[{"overlay_id":"sub2api-stock-v0129-kimi-a7m","display_name":"sub2api stock v0.1.129 Kimi A7M overlay","target_host":"sub2api","min_host_version":"0.1.129","max_host_version":"0.1.129","patch_path":"overlays/missing.patch","reason":"stock host still routes chat traffic into unsupported Responses path"}],"group_template":{"name":"g","rate_multiplier":1},"channel_template":{"name":"c","model_mapping":{"kimi-k2.6":"kimi-k2.6"}},"plan_template":{"name":"p","price":1,"validity_days":30,"validity_unit":"day"},"import":{"supports_multi_key":true,"supports_strict":true,"supports_partial":true}}`,
	})

	_, err := LoadDir(packDir)
	if err == nil || !strings.Contains(err.Error(), `read file reference "overlays/missing.patch"`) {
		t.Fatalf("LoadDir() error = %v, want missing overlay file detail", err)
	}
}

func createPackFixture(t *testing.T, files map[string]string) string {
	t.Helper()

	packDir := t.TempDir()
	var lines []string
	for relativePath, content := range files {
		absolutePath := filepath.Join(packDir, relativePath)
		mustWrite(t, absolutePath, content)
		sum := sha256.Sum256([]byte(content))
		lines = append(lines, hex.EncodeToString(sum[:])+"  "+relativePath)
	}
	mustWrite(t, filepath.Join(packDir, "checksums.txt"), strings.Join(lines, "\n")+"\n")
	return packDir
}

func mustWrite(t *testing.T, path string, content string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("MkdirAll(%q) error = %v", path, err)
	}
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("WriteFile(%q) error = %v", path, err)
	}
}
