package config

import (
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/alecthomas/units"
	"sigs.k8s.io/yaml"
)

func TestParseUsesDefaultsAndRejectsUnknownFields(t *testing.T) {
	configured, err := Parse([]byte("{}\n"))
	if err != nil {
		t.Fatalf("Parse() error = %v", err)
	}
	if configured.Scrapers.Interval.Duration != 15*time.Second {
		t.Fatalf("scrapers.interval = %s, want 15s", configured.Scrapers.Interval.Duration)
	}
	if configured.Metrics.BindAddress != "0" {
		t.Fatalf("metrics.bindAddress = %q, want 0", configured.Metrics.BindAddress)
	}
	if configured.Records.Dir != "/tmp/capacity-planner-records" {
		t.Fatalf("records.dir = %q, want /tmp/capacity-planner-records", configured.Records.Dir)
	}
	if _, err := Parse([]byte("unknown: true\n")); err == nil || !strings.Contains(err.Error(), "unknown field") {
		t.Fatalf("Parse(unknown) error = %v, want unknown field error", err)
	}
}

func TestParseHumanReadableValues(t *testing.T) {
	configured, err := Parse([]byte(`
records:
  dir: /tmp/decisions
scrapers:
  interval: 30s
  podMetrics:
    maxBodySize: 64MiB
rules:
  records:
  - record: aggregate_metric
    expr: sum(source_metric)
    labels:
      namespace: fixed
`))
	if err != nil {
		t.Fatalf("Parse() error = %v", err)
	}
	if configured.Scrapers.Interval.Duration != 30*time.Second {
		t.Fatalf("scrapers.interval = %s, want 30s", configured.Scrapers.Interval.Duration)
	}
	if configured.Records.Dir != "/tmp/decisions" {
		t.Fatalf("records.dir = %q, want /tmp/decisions", configured.Records.Dir)
	}
	if configured.Scrapers.PodMetrics.MaxBodySize != 64*units.MiB {
		t.Fatalf("scrapers.podMetrics.maxBodySize = %s, want 64MiB", configured.Scrapers.PodMetrics.MaxBodySize)
	}
	if len(configured.Rules.Records) != 1 || configured.Rules.Records[0].Labels["namespace"] != "fixed" {
		t.Fatalf("rules.records = %+v, want one generic recording rule", configured.Rules.Records)
	}
}

func TestValidateRejectsInvalidConfig(t *testing.T) {
	tests := []struct {
		name   string
		mutate func(*Config)
		want   string
	}{
		{name: "api version", mutate: func(c *Config) { c.APIVersion = "v2" }, want: "apiVersion"},
		{name: "kind", mutate: func(c *Config) { c.Kind = "Other" }, want: "kind"},
		{name: "sync period", mutate: func(c *Config) { c.Controller.SyncPeriod.Duration = 0 }, want: "controller.syncPeriod"},
		{name: "health address", mutate: func(c *Config) { c.Server.Health.BindAddress = "" }, want: "server.health.bindAddress"},
		{name: "self metrics address", mutate: func(c *Config) { c.Metrics.BindAddress = "" }, want: "metrics.bindAddress"},
		{name: "storage path", mutate: func(c *Config) { c.Storage.Path = "" }, want: "storage.path"},
		{name: "query samples", mutate: func(c *Config) { c.Query.MaxSamples = 0 }, want: "query.maxSamples"},
		{name: "scrape interval", mutate: func(c *Config) { c.Scrapers.Interval.Duration = 0 }, want: "scrapers.interval"},
		{name: "pod body size", mutate: func(c *Config) { c.Scrapers.PodMetrics.MaxBodySize = 0 }, want: "scrapers.podMetrics.maxBodySize"},
		{name: "pod metric", mutate: func(c *Config) { c.Scrapers.PodMetrics.Metrics = []string{"bad-metric"} }, want: "invalid metric"},
		{name: "watcher JSONPath", mutate: func(c *Config) { c.Scrapers.PodWatcher.Metrics[0].JSONPath = "{" }, want: "outer braces"},
		{name: "rule expression", mutate: func(c *Config) {
			c.Rules.Records = []RecordingRule{{Record: "derived", Expr: "sum ("}}
		}, want: "invalid PromQL"},
		{name: "rule metric label", mutate: func(c *Config) {
			c.Rules.Records = []RecordingRule{{Record: "derived", Expr: "vector(1)", Labels: map[string]string{"__name__": "other"}}}
		}, want: "metric name cannot be overridden"},
		{name: "logging encoder", mutate: func(c *Config) { c.Logging.Encoder = "xml" }, want: "logging.encoder"},
		{name: "logging level", mutate: func(c *Config) { c.Logging.Level = "verbose" }, want: "logging.level"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configured := Default()
			tt.mutate(configured)
			if err := configured.Validate(); err == nil || !strings.Contains(err.Error(), tt.want) {
				t.Fatalf("Validate() error = %v, want %q", err, tt.want)
			}
		})
	}
}

func TestCanonicalConfigMatchesHelmValues(t *testing.T) {
	moduleRoot := filepath.Join("..", "..")
	canonical, err := LoadFile(filepath.Join(moduleRoot, "config", "capacity-planner.yaml"))
	if err != nil {
		t.Fatal(err)
	}
	valuesData, err := os.ReadFile(filepath.Join(moduleRoot, "charts", "capacity-planner", "values.yaml"))
	if err != nil {
		t.Fatal(err)
	}
	var values struct {
		Config Config `json:"config"`
	}
	if err := yaml.Unmarshal(valuesData, &values); err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(canonical, &values.Config) {
		t.Fatalf("Helm runtime config differs from canonical config\ncanonical: %+v\nHelm: %+v", canonical, &values.Config)
	}
}
