package pack

import (
	"archive/zip"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestValidateManifestRequiredFields(t *testing.T) {
	tests := []struct {
		name     string
		manifest Manifest
		wantErr  string
	}{
		{
			name: "empty pack id",
			manifest: Manifest{
				Version:      "1.0.0",
				TargetHost:   "sub2api",
				ProvidersDir: "providers",
				ChecksumFile: "checksums.txt",
			},
			wantErr: "pack_id is required",
		},
		{
			name: "empty version",
			manifest: Manifest{
				PackID:       "openai-cn-pack",
				TargetHost:   "sub2api",
				ProvidersDir: "providers",
				ChecksumFile: "checksums.txt",
			},
			wantErr: "version is required",
		},
		{
			name: "empty target host",
			manifest: Manifest{
				PackID:       "openai-cn-pack",
				Version:      "1.0.0",
				ProvidersDir: "providers",
				ChecksumFile: "checksums.txt",
			},
			wantErr: "target_host is required",
		},
		{
			name: "empty providers dir",
			manifest: Manifest{
				PackID:       "openai-cn-pack",
				Version:      "1.0.0",
				TargetHost:   "sub2api",
				ChecksumFile: "checksums.txt",
			},
			wantErr: "providers_dir is required",
		},
		{
			name: "empty checksum file",
			manifest: Manifest{
				PackID:       "openai-cn-pack",
				Version:      "1.0.0",
				TargetHost:   "sub2api",
				ProvidersDir: "providers",
			},
			wantErr: "checksum_file is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateManifest(tt.manifest)
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("validateManifest() error = %v, want substring %q", err, tt.wantErr)
			}
		})
	}
}

func TestValidateProvidersRejectsInvalidProviderFields(t *testing.T) {
	tests := []struct {
		name      string
		providers []ProviderManifest
		wantErr   string
	}{
		{
			name: "empty provider id",
			providers: []ProviderManifest{{
				DisplayName:    "DeepSeek",
				BaseURL:        "https://api.deepseek.com",
				Platform:       "openai",
				AccountType:    "apikey",
				DefaultModels:  []string{"deepseek-chat"},
				SmokeTestModel: "deepseek-chat",
				GroupTemplate:  GroupTemplate{Name: "g"},
				ChannelTemplate: ChannelTemplate{
					Name:         "c",
					ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
				},
				PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
			}},
			wantErr: "provider_id is required",
		},
		{
			name: "empty base url",
			providers: []ProviderManifest{{
				ProviderID:     "deepseek",
				DisplayName:    "DeepSeek",
				BaseURL:        "",
				Platform:       "openai",
				AccountType:    "apikey",
				DefaultModels:  []string{"deepseek-chat"},
				SmokeTestModel: "deepseek-chat",
				GroupTemplate:  GroupTemplate{Name: "g"},
				ChannelTemplate: ChannelTemplate{
					Name:         "c",
					ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
				},
				PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
			}},
			wantErr: "base_url must use https",
		},
		{
			name: "missing display name",
			providers: []ProviderManifest{{
				ProviderID:     "deepseek",
				DisplayName:    "",
				BaseURL:        "https://api.deepseek.com",
				Platform:       "openai",
				AccountType:    "apikey",
				DefaultModels:  []string{"deepseek-chat"},
				SmokeTestModel: "deepseek-chat",
				GroupTemplate:  GroupTemplate{Name: "g"},
				ChannelTemplate: ChannelTemplate{
					Name:         "c",
					ModelMapping: map[string]string{"deepseek-chat": "deepseek-chat"},
				},
				PlanTemplate: PlanTemplate{Name: "p", ValidityDays: 30},
			}},
			wantErr: "display_name is required",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateProviders(t.TempDir(), tt.providers)
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("validateProviders() error = %v, want substring %q", err, tt.wantErr)
			}
		})
	}
}

func TestExtractZipToTempRejectsEmptyArchive(t *testing.T) {
	archivePath := filepath.Join(t.TempDir(), "empty.zip")
	file, err := os.Create(archivePath)
	if err != nil {
		t.Fatalf("os.Create() error = %v", err)
	}
	writer := zip.NewWriter(file)
	if err := writer.Close(); err != nil {
		t.Fatalf("writer.Close() error = %v", err)
	}
	if err := file.Close(); err != nil {
		t.Fatalf("file.Close() error = %v", err)
	}

	_, cleanup, err := extractZipToTemp(archivePath)
	if cleanup != nil {
		cleanup()
	}
	if err == nil || !strings.Contains(err.Error(), "pack archive is empty") {
		t.Fatalf("extractZipToTemp() error = %v, want empty archive error", err)
	}
}

func TestExtractZipToTempRejectsPathTraversal(t *testing.T) {
	archivePath := filepath.Join(t.TempDir(), "traversal.zip")
	file, err := os.Create(archivePath)
	if err != nil {
		t.Fatalf("os.Create() error = %v", err)
	}
	writer := zip.NewWriter(file)
	entry, err := writer.Create("../../../evil.txt")
	if err != nil {
		t.Fatalf("writer.Create() error = %v", err)
	}
	if _, err := entry.Write([]byte("evil")); err != nil {
		t.Fatalf("entry.Write() error = %v", err)
	}
	if err := writer.Close(); err != nil {
		t.Fatalf("writer.Close() error = %v", err)
	}
	if err := file.Close(); err != nil {
		t.Fatalf("file.Close() error = %v", err)
	}

	_, cleanup, err := extractZipToTemp(archivePath)
	if cleanup != nil {
		cleanup()
	}
	if err == nil || !strings.Contains(err.Error(), "invalid path") {
		t.Fatalf("extractZipToTemp() error = %v, want invalid path error", err)
	}
}

func TestMatchesMaxConstraintCases(t *testing.T) {
	tests := []struct {
		name        string
		hostVersion string
		maxVersion  string
		want        bool
	}{
		{name: "exact version match", hostVersion: "1.2.3", maxVersion: "1.2.3", want: true},
		{name: "wildcard x accepts same minor", hostVersion: "0.2.9", maxVersion: "0.2.x", want: true},
		{name: "non matching version", hostVersion: "1.2.4", maxVersion: "1.2.3", want: false},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := matchesMaxConstraint(tt.hostVersion, tt.maxVersion)
			if err != nil {
				t.Fatalf("matchesMaxConstraint() error = %v", err)
			}
			if got != tt.want {
				t.Fatalf("matchesMaxConstraint() = %v, want %v", got, tt.want)
			}
		})
	}
}

func TestMatchesMaxConstraintRejectsWildcardStar(t *testing.T) {
	_, err := matchesMaxConstraint("1.2.3", "1.2.*")
	if err == nil || !strings.Contains(err.Error(), `parse version segment "*"`) {
		t.Fatalf("matchesMaxConstraint() error = %v, want parse failure for wildcard star", err)
	}
}

func TestLoadPathRejectsEmptyAndMissingPath(t *testing.T) {
	tests := []struct {
		name    string
		path    string
		wantErr string
	}{
		{name: "empty path", path: "   ", wantErr: "pack path is required"},
		{name: "missing path", path: filepath.Join(t.TempDir(), "missing-pack"), wantErr: "stat pack path"},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			_, err := LoadPath(tt.path)
			if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
				t.Fatalf("LoadPath() error = %v, want substring %q", err, tt.wantErr)
			}
		})
	}
}

func TestLoadArchiveRejectsNonZipFile(t *testing.T) {
	filePath := filepath.Join(t.TempDir(), "not-a-zip.zip")
	mustWrite(t, filePath, "plain text, not a zip archive")

	_, err := LoadArchive(filePath)
	if err == nil || !strings.Contains(err.Error(), "open pack archive") {
		t.Fatalf("LoadArchive() error = %v, want open archive error", err)
	}
}
