package schema

import (
	"slices"
	"testing"
)

func obj(pairs ...any) map[string]any {
	m := map[string]any{}
	for i := 0; i+1 < len(pairs); i += 2 {
		m[pairs[i].(string)] = pairs[i+1]
	}
	return m
}

func messages(vs []Violation) []string {
	out := make([]string, 0, len(vs))
	for _, v := range vs {
		out = append(out, v.String())
	}
	return out
}

func TestValidate(t *testing.T) {
	cases := []struct {
		name   string
		schema map[string]any
		value  any
		want   []string // "yol: mesaj" biçiminde
	}{
		{
			name:   "geçerli nesne",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string")), "required", []any{"a"}),
			value:  obj("a", "merhaba"),
		},
		{
			name:   "tip uyuşmazlığı alt alana inmez",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string")), "required", []any{"a"}),
			value:  obj("a", 12),
			want:   []string{`a: tipi string olmalı (alınan: number)`},
		},
		{
			name:   "kök tipi yanlış",
			schema: obj("type", "object"),
			value:  "metin",
			want:   []string{`tipi object olmalı (alınan: string)`},
		},
		{
			name:   "zorunlu alan eksik",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string"), "b", obj("type", "number")), "required", []any{"a", "b"}),
			value:  obj("a", "x"),
			want:   []string{`b: zorunlu alan`},
		},
		{
			name:   "tanımsız alan kapalıysa reddedilir",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string")), "additionalProperties", false),
			value:  obj("a", "x", "fazla", 1),
			want:   []string{`fazla: tanımsız alan`},
		},
		{
			name:   "tanımsız alan açıksa geçer",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string"))),
			value:  obj("a", "x", "fazla", 1),
		},
		{
			name:   "enum",
			schema: obj("type", "object", "properties", obj("mode", obj("type", "string", "enum", []any{"hybrid", "keyword"}))),
			value:  obj("mode", "semantic"),
			want:   []string{`mode: şu değerlerden biri olmalı: "hybrid", "keyword"`},
		},
		{
			name:   "sayı aralığı",
			schema: obj("type", "object", "properties", obj("offset", obj("type", "number", "minimum", -50.0, "maximum", 50.0))),
			value:  obj("offset", 100),
			want:   []string{`offset: en fazla 50 olmalı`},
		},
		{
			name:   "integer kırılma değerini reddeder",
			schema: obj("type", "object", "properties", obj("count", obj("type", "integer"))),
			value:  obj("count", 3.5),
			want:   []string{`count: tipi integer olmalı (alınan: number)`},
		},
		{
			name:   "bool sayı sayılmaz",
			schema: obj("type", "object", "properties", obj("n", obj("type", "number"))),
			value:  obj("n", true),
			want:   []string{`n: tipi number olmalı (alınan: boolean)`},
		},
		{
			name: "dizi elemanları",
			schema: obj("type", "object", "properties", obj("points",
				obj("type", "array", "minItems", 1, "maxItems", 2, "items", obj("type", "number")))),
			value: obj("points", []any{1, "x", 3}),
			want: []string{
				`points: en fazla 2 öğe olmalı`,
				`points[1]: tipi number olmalı (alınan: string)`,
			},
		},
		{
			name:   "birden çok tip kabul edilir",
			schema: obj("type", "object", "properties", obj("v", obj("type", []any{"string", "number"}))),
			value:  obj("v", 3),
		},
		{
			name:   "tüm hatalar birlikte döner ve sıra kararlıdır",
			schema: obj("type", "object", "properties", obj("a", obj("type", "string"), "b", obj("type", "number"), "c", obj("type", "boolean")), "required", []any{"c"}),
			value:  obj("a", 1, "b", "x"),
			want: []string{
				`a: tipi string olmalı (alınan: number)`,
				`b: tipi number olmalı (alınan: string)`,
				`c: zorunlu alan`,
			},
		},
		{
			name:   "metin uzunluğu rune ile ölçülür",
			schema: obj("type", "object", "properties", obj("ad", obj("type", "string", "minLength", 3))),
			value:  obj("ad", "ışık"), // 4 rune, 5 bayt
		},
		{
			name:   "desen",
			schema: obj("type", "object", "properties", obj("ad", obj("type", "string", "pattern", "^[a-z_]+$"))),
			value:  obj("ad", "Türkçe"),
			want:   []string{`ad: "^[a-z_]+$" desenine uymuyor`},
		},
		{
			name:   "geçersiz desen şemayı değil girdiyi reddetmez",
			schema: obj("type", "object", "properties", obj("ad", obj("type", "string", "pattern", "["))),
			value:  obj("ad", "her şey"),
		},
		{
			name:   "şema yoksa serbest",
			schema: nil,
			value:  obj("a", 1),
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			got := messages(Validate(tc.schema, tc.value))
			// slices.Equal: boş liste ile nil eşdeğerdir (yoksa "geçerli" vakalar hata görünür).
			if !slices.Equal(got, tc.want) {
				t.Errorf("got %v, want %v", got, tc.want)
			}
			// Aynı girdi aynı listeyi vermeli (harita sırası rastgele olabilirdi).
			if again := messages(Validate(tc.schema, tc.value)); !slices.Equal(again, got) {
				t.Errorf("ikinci koşu farklı: %v != %v", again, got)
			}
		})
	}
}

func TestApplyDefaults(t *testing.T) {
	sch := obj("type", "object", "properties", obj(
		"offset", obj("type", "number", "default", 30),
		"name", obj("type", "string"),
		"nested", obj("type", "object", "properties", obj("on", obj("type", "boolean", "default", true))),
	))
	in := obj("name", "deneme", "nested", obj("kendi", 1))

	got := ApplyDefaults(sch, in).(map[string]any)

	if got["offset"] != 30 {
		t.Errorf("varsayılan uygulanmadı: %v", got["offset"])
	}
	if got["name"] != "deneme" {
		t.Errorf("mevcut değer ezmelidi: %v", got["name"])
	}
	nested := got["nested"].(map[string]any)
	if nested["on"] != true || nested["kendi"] != 1 {
		t.Errorf("iç varsayılan uygulanmadı: %v", nested)
	}
	// Girdi DEĞİŞTİRİLMEDİ: çağıranın haritası bizim kopyamızı paylaşmasın.
	if _, ok := in["offset"]; ok {
		t.Error("ApplyDefaults çağıranın girdisini değiştirdi")
	}
	if _, ok := in["nested"].(map[string]any)["on"]; ok {
		t.Error("ApplyDefaults iç nesneyi değiştirdi")
	}
}

func TestValidateAfterDefaults(t *testing.T) {
	// Gerçek akış: varsayılanlar uygulanır, SONRA doğrulama yapılır. Sıra tersine
	// çevrilirse "zorunlu alan" hatası yanlışlıkla üretilir.
	sch := obj("type", "object",
		"properties", obj("offset", obj("type", "number", "default", 30)),
		"required", []any{"offset"},
	)
	if got := Validate(sch, ApplyDefaults(sch, map[string]any{})); len(got) != 0 {
		t.Errorf("varsayılan sonrası doğrulama geçmeliydi: %v", got)
	}
}

func TestNumberAsString(t *testing.T) {
	// Elle yazılmış şemalarda "5" yazılmış olabilir; kısıt yine uygulanmalı.
	sch := obj("type", "object", "properties", obj("n", obj("type", "number", "maximum", "10")))
	got := messages(Validate(sch, obj("n", 11)))
	want := []string{`n: en fazla 10 olmalı`}
	if !slices.Equal(got, want) {
		t.Errorf("got %v, want %v", got, want)
	}
}
