package config

import (
	"strings"
	"testing"

	"gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline"
)

func TestValidateConcurrencyRules(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*Config)
		want  string
	}{
		{
			name: "non-positive total",
			setup: func(cfg *Config) {
				cfg.Concurrency.TotalLLM = 0
			},
			want: "total LLM concurrency must be greater than zero",
		},
		{
			name: "worker below one",
			setup: func(cfg *Config) {
				cfg.Concurrency.TotalLLM = 3
				cfg.Concurrency.StageWorkers = map[string]int{"extract": 0}
				cfg.Concurrency.extractWorkersConfigured = true
			},
			want: "stage_workers.extract must be between 1",
		},
		{
			name: "worker above total",
			setup: func(cfg *Config) {
				cfg.Concurrency.TotalLLM = 3
				cfg.Concurrency.StageWorkers = map[string]int{"extract": 4}
				cfg.Concurrency.extractWorkersConfigured = true
			},
			want: "stage_workers.extract must be between 1",
		},
		{
			name: "worker lower boundary",
			setup: func(cfg *Config) {
				cfg.Concurrency.TotalLLM = 3
				cfg.Concurrency.StageWorkers = map[string]int{"extract": 1}
				cfg.Concurrency.extractWorkersConfigured = true
			},
		},
		{
			name: "worker upper boundary",
			setup: func(cfg *Config) {
				cfg.Concurrency.TotalLLM = 3
				cfg.Concurrency.StageWorkers = map[string]int{"extract": 3}
				cfg.Concurrency.extractWorkersConfigured = true
			},
		},
		{
			name: "unknown worker key",
			setup: func(cfg *Config) {
				cfg.Concurrency.StageWorkers = map[string]int{"worker": 1}
			},
			want: "stage_workers key \"worker\" is not supported",
		},
		{
			name: "blank worker key",
			setup: func(cfg *Config) {
				cfg.Concurrency.StageWorkers = map[string]int{" ": 1}
			},
			want: "stage_workers key must not be empty",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			tt.setup(&cfg)
			err := cfg.Validate()
			if tt.want == "" {
				if err != nil {
					t.Fatalf("Validate() error = %v", err)
				}
				return
			}
			if err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("Validate() error = %v, want context %q", err, tt.want)
			}
		})
	}
}

func TestValidatePromptKitSourcesAreMutuallyExclusive(t *testing.T) {
	cfg := Default()
	cfg.PromptKit = PromptKitConfig{ProfileDir: "./profiles", ProfileFile: "./profile.yml"}
	assertValidationContains(t, cfg, "promptkit profile_dir and profile_file are mutually exclusive")
}

func TestValidatePromptKitLocalBackendEndpoints(t *testing.T) {
	tests := []struct {
		name          string
		endpoint      string
		profileSource PromptKitConfig
	}{
		{
			name:          "HTTP endpoint with path and profile directory",
			endpoint:      "http://localhost:8000/v1",
			profileSource: PromptKitConfig{ProfileDir: "./profiles"},
		},
		{
			name:          "case-insensitive HTTPS endpoint and profile file",
			endpoint:      "HTTPS://inference.example.test/api",
			profileSource: PromptKitConfig{ProfileFile: "./profiles.yml"},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			cfg.PromptKit = tt.profileSource
			cfg.PromptKit.LocalBackend = &PromptKitLocalBackendConfig{
				Endpoint:         tt.endpoint,
				ConcurrencyLimit: 2,
			}
			if err := cfg.Validate(); err != nil {
				t.Fatalf("Validate() error = %v", err)
			}
		})
	}
}

func TestValidatePromptKitLocalBackendRejectsInvalidValues(t *testing.T) {
	tests := []struct {
		name             string
		endpoint         string
		concurrencyLimit int
		want             string
	}{
		{name: "blank endpoint", endpoint: "   ", want: "promptkit.local_backend.endpoint"},
		{name: "relative URL", endpoint: "localhost:8000/v1", want: "promptkit.local_backend.endpoint"},
		{name: "unsupported scheme", endpoint: "ftp://localhost/model", want: "promptkit.local_backend.endpoint"},
		{name: "missing host", endpoint: "http:///v1", want: "promptkit.local_backend.endpoint"},
		{name: "user information", endpoint: "http://user:secret@localhost/v1", want: "promptkit.local_backend.endpoint"},
		{name: "query", endpoint: "http://localhost/v1?model=example", want: "promptkit.local_backend.endpoint"},
		{name: "empty query", endpoint: "http://localhost/v1?", want: "promptkit.local_backend.endpoint"},
		{name: "fragment", endpoint: "http://localhost/v1#model", want: "promptkit.local_backend.endpoint"},
		{name: "empty fragment", endpoint: "http://localhost/v1#", want: "promptkit.local_backend.endpoint"},
		{
			name:             "negative concurrency",
			endpoint:         "http://localhost:8000/v1",
			concurrencyLimit: -1,
			want:             "promptkit.local_backend.concurrency_limit",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			cfg.PromptKit.LocalBackend = &PromptKitLocalBackendConfig{
				Endpoint:         tt.endpoint,
				ConcurrencyLimit: tt.concurrencyLimit,
			}
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateStateSurfaceRules(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*Config)
		want  string
	}{
		{
			name: "blank output root",
			setup: func(cfg *Config) {
				cfg.Output.Directory = "  "
			},
			want: "output.directory must not be empty",
		},
		{
			name: "blank debug root",
			setup: func(cfg *Config) {
				cfg.Debug.Directory = "  "
			},
			want: "debug.directory must not be empty",
		},
		{
			name: "NUL in output root",
			setup: func(cfg *Config) {
				cfg.Output.Directory = "./out\x00put"
			},
			want: "output.directory must not contain NUL",
		},
		{
			name: "NUL in chunk plan root",
			setup: func(cfg *Config) {
				cfg.Cache.ChunkPlans.Directory = "./plans\x00"
			},
			want: "cache.chunk_plans.directory must not contain NUL",
		},
		{
			name: "NUL in checkpoint root",
			setup: func(cfg *Config) {
				cfg.Cache.Checkpoints.Directory = "./checkpoints\x00"
			},
			want: "cache.checkpoints.directory must not contain NUL",
		},
		{
			name: "NUL in debug root",
			setup: func(cfg *Config) {
				cfg.Debug.Directory = "./debug\x00"
			},
			want: "debug.directory must not contain NUL",
		},
		{
			name: "invalid chunk plan mode",
			setup: func(cfg *Config) {
				cfg.Cache.ChunkPlans.Mode = pipeline.ChunkCacheMode("invalid")
			},
			want: "cache.chunk_plans.mode:",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			tt.setup(&cfg)
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateIdentifiersAfterTrimming(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*Config)
		want  string
	}{
		{
			name: "empty pipeline id",
			setup: func(cfg *Config) {
				cfg.Pipelines = map[string]pipeline.PipelineProfile{" ": {}}
			},
			want: "pipeline id must not be empty",
		},
		{
			name: "duplicate pipeline ids",
			setup: func(cfg *Config) {
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": {}, " main ": {}}
			},
			want: "pipeline id \"main\" is duplicated after trimming",
		},
		{
			name: "empty lane id",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.Artifacts = map[string]pipeline.ArtifactLaneProfile{" ": {}}
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "artifact lane id must not be empty",
		},
		{
			name: "duplicate lane ids",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.Artifacts = map[string]pipeline.ArtifactLaneProfile{"spells": {}, " spells ": {}}
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "artifact lane id \"spells\" is duplicated after trimming",
		},
		{
			name: "empty reference slot",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.References = pipeline.ExternalReferenceMap(map[string]string{" ": "source.txt"})
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "reference slot name must not be empty",
		},
		{
			name: "duplicate reference slots",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "one.txt", " slot ": "two.txt"})
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "reference slot \"slot\" is duplicated after trimming",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			tt.setup(&cfg)
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateBindingRetriesAndProfiles(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*Config)
		want  string
	}{
		{
			name: "negative retries",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.Input.Retries = -1
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "input retries must be greater than or equal to zero",
		},
		{
			name: "whitespace-only input profile",
			setup: func(cfg *Config) {
				profile := validationProfile()
				profile.Input.LLMProfile = "   "
				cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			},
			want: "input llm_profile must not be empty when set",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			cfg := Default()
			tt.setup(&cfg)
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateReferencesAreUnsupportedOnInputAndOutput(t *testing.T) {
	tests := []struct {
		name string
		set  func(*pipeline.PipelineProfile)
		want string
	}{
		{
			name: "input references",
			set: func(profile *pipeline.PipelineProfile) {
				profile.Input.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"})
			},
			want: "input references are not supported",
		},
		{
			name: "output references",
			set: func(profile *pipeline.PipelineProfile) {
				profile.Output.References = pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"})
			},
			want: "output references are not supported",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile := validationProfile()
			tt.set(&profile)
			cfg := Default()
			cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateValidatorBindingRules(t *testing.T) {
	tests := []struct {
		name  string
		setup func(*pipeline.PipelineProfile)
		want  string
	}{
		{
			name: "empty validator module",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Chunk.Validators = pipeline.ValidatorOverride{
					Set:        true,
					Validators: []pipeline.ModuleBinding{{}},
				}
			},
			want: "chunk validators[0] module must not be empty",
		},
		{
			name: "negative validator retries",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Chunk.Validators = pipeline.ValidatorOverride{
					Set: true,
					Validators: []pipeline.ModuleBinding{{
						Module:  "validator",
						Retries: -1,
					}},
				}
			},
			want: "chunk validators[0] retries must be greater than or equal to zero",
		},
		{
			name: "validator references",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Chunk.Validators = pipeline.ValidatorOverride{
					Set: true,
					Validators: []pipeline.ModuleBinding{{
						Module:     "validator",
						References: pipeline.ExternalReferenceMap(map[string]string{"slot": "source.txt"}),
					}},
				}
			},
			want: "chunk validators[0] references are not supported",
		},
		{
			name: "nested validators",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Chunk.Validators = pipeline.ValidatorOverride{
					Set: true,
					Validators: []pipeline.ModuleBinding{{
						Module:     "validator",
						Validators: pipeline.ValidatorOverride{Set: true},
					}},
				}
			},
			want: "chunk validators[0] nested validators are not supported",
		},
		{
			name: "input validator chain",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Input.Validators = pipeline.ValidatorOverride{Set: true}
			},
			want: "input validators are not supported",
		},
		{
			name: "output validator chain",
			setup: func(profile *pipeline.PipelineProfile) {
				profile.Output.Validators = pipeline.ValidatorOverride{Set: true}
			},
			want: "output validators are not supported",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile := validationProfile()
			tt.setup(&profile)
			cfg := Default()
			cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			assertValidationContains(t, cfg, tt.want)
		})
	}
}

func TestValidateLaneValidatorCompatibility(t *testing.T) {
	tests := []struct {
		name string
		lane func(*pipeline.ArtifactLaneProfile)
		want string
	}{
		{
			name: "deprecated non-empty lane validators",
			lane: func(lane *pipeline.ArtifactLaneProfile) {
				lane.Validators = []pipeline.ModuleBinding{{Module: "old-validator"}}
			},
			want: "validators are not supported at artifact lane level",
		},
		{
			name: "stage validators omitted",
			lane: func(lane *pipeline.ArtifactLaneProfile) {
			},
		},
		{
			name: "stage validators explicitly empty",
			lane: func(lane *pipeline.ArtifactLaneProfile) {
				lane.Extract.Validators = pipeline.ValidatorOverride{Set: true}
			},
		},
		{
			name: "stage validators configured",
			lane: func(lane *pipeline.ArtifactLaneProfile) {
				lane.Extract.Validators = pipeline.ValidatorOverride{
					Set:        true,
					Validators: []pipeline.ModuleBinding{{Module: "validator"}},
				}
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			profile := validationProfile()
			lane := profile.Artifacts["lane"]
			tt.lane(&lane)
			profile.Artifacts["lane"] = lane
			cfg := Default()
			cfg.Pipelines = map[string]pipeline.PipelineProfile{"main": profile}
			err := cfg.Validate()
			if tt.want == "" {
				if err != nil {
					t.Fatalf("Validate() error = %v", err)
				}
				return
			}
			if err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("Validate() error = %v, want context %q", err, tt.want)
			}
		})
	}
}

func validationProfile() pipeline.PipelineProfile {
	return pipeline.PipelineProfile{
		ID:    "main",
		Input: pipeline.Binding("input"),
		Artifacts: map[string]pipeline.ArtifactLaneProfile{
			"lane": {},
		},
	}
}

func assertValidationContains(t *testing.T, cfg Config, want string) {
	t.Helper()
	err := cfg.Validate()
	if err == nil || !strings.Contains(err.Error(), want) {
		t.Fatalf("Validate() error = %v, want context %q", err, want)
	}
}
