package jsonschema

import (
	"encoding/json"
	"testing"
)

// fl returns a *float64 pointing at v — convenience for table tests.
func fl(v float64) *float64 { return &v }
func ip(v int) *int         { return &v }

// jsonAny decodes a JSON literal into the same `any` shape that the form
// handler hands to Validate (json.Unmarshal of the request body).
func jsonAny(t *testing.T, s string) any {
	t.Helper()
	var v any
	if err := json.Unmarshal([]byte(s), &v); err != nil {
		t.Fatalf("decode %q: %v", s, err)
	}
	return v
}

func TestValidate_Types(t *testing.T) {
	cases := []struct {
		name    string
		schema  *Schema
		valueJS string
		wantErr bool
	}{
		{"string-ok", &Schema{Type: "string"}, `"hi"`, false},
		{"string-wrong-type", &Schema{Type: "string"}, `42`, true},
		{"number-ok-int", &Schema{Type: "number"}, `42`, false},
		{"number-ok-float", &Schema{Type: "number"}, `3.14`, false},
		{"number-wrong-type", &Schema{Type: "number"}, `"hi"`, true},
		{"integer-ok", &Schema{Type: "integer"}, `42`, false},
		{"integer-rejects-float", &Schema{Type: "integer"}, `3.14`, true},
		{"integer-accepts-floaty-int", &Schema{Type: "integer"}, `42.0`, false},
		{"boolean-ok", &Schema{Type: "boolean"}, `true`, false},
		{"boolean-wrong-type", &Schema{Type: "boolean"}, `"true"`, true},
		{"null-ok", &Schema{Type: "null"}, `null`, false},
		{"array-ok", &Schema{Type: "array"}, `[]`, false},
		{"array-wrong-type", &Schema{Type: "array"}, `{}`, true},
		{"object-ok", &Schema{Type: "object"}, `{}`, false},
		{"object-wrong-type", &Schema{Type: "object"}, `[]`, true},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			errs := Validate(tc.schema, jsonAny(t, tc.valueJS))
			if (len(errs) > 0) != tc.wantErr {
				t.Fatalf("Validate errs = %v; wantErr=%v", errs, tc.wantErr)
			}
		})
	}
}

func TestValidate_Enum(t *testing.T) {
	s := &Schema{Type: "string", Enum: []any{"a", "b", "c"}}

	if errs := Validate(s, "a"); len(errs) != 0 {
		t.Errorf("a: unexpected errs %v", errs)
	}
	if errs := Validate(s, "z"); len(errs) == 0 {
		t.Errorf("z: expected enum error")
	}

	// Numeric enum with int↔float coercion: schema authored with int, value
	// arrives as float64 (JSON default).
	num := &Schema{Type: "integer", Enum: []any{1, 2, 3}}
	if errs := Validate(num, jsonAny(t, `2`)); len(errs) != 0 {
		t.Errorf("int 2 against int enum: unexpected errs %v", errs)
	}
	if errs := Validate(num, jsonAny(t, `4`)); len(errs) == 0 {
		t.Errorf("int 4 against int enum: expected error")
	}
}

func TestValidate_StringConstraints(t *testing.T) {
	s := &Schema{Type: "string", MinLength: ip(3), MaxLength: ip(8)}
	cases := map[string]int{
		`"hi"`:        1, // too short
		`"hello"`:     0,
		`"hellothere"`: 1, // too long
	}
	for v, want := range cases {
		errs := Validate(s, jsonAny(t, v))
		if len(errs) != want {
			t.Errorf("%s: got %d errs (%v), want %d", v, len(errs), errs, want)
		}
	}
}

func TestValidate_NumberConstraints(t *testing.T) {
	s := &Schema{Type: "integer", Minimum: fl(1), Maximum: fl(5)}
	cases := []struct {
		v    string
		want int
	}{
		{`0`, 1}, {`1`, 0}, {`3`, 0}, {`5`, 0}, {`6`, 1},
	}
	for _, tc := range cases {
		errs := Validate(s, jsonAny(t, tc.v))
		if len(errs) != tc.want {
			t.Errorf("v=%s: got %d errs (%v), want %d", tc.v, len(errs), errs, tc.want)
		}
	}
}

func TestValidate_Format_Date(t *testing.T) {
	s := &Schema{Type: "string", Format: "date"}
	cases := map[string]bool{
		`"2026-05-01"`: false,
		`"2026-13-01"`: true, // bad month
		`"2026-02-30"`: true, // bad day
		`"05/01/2026"`: true, // wrong format
		`"2026-5-1"`:   true, // missing zero-pad
	}
	for v, wantErr := range cases {
		errs := Validate(s, jsonAny(t, v))
		if (len(errs) > 0) != wantErr {
			t.Errorf("%s: got errs=%v wantErr=%v", v, errs, wantErr)
		}
	}
}

func TestValidate_Format_Email(t *testing.T) {
	s := &Schema{Type: "string", Format: "email"}
	cases := map[string]bool{
		`"casey@example.com"`: false,
		`"casey+tag@ex.io"`:   false,
		`"not-an-email"`:      true,
		`"missing@dot"`:       true,
		`"@nouser.com"`:       true,
	}
	for v, wantErr := range cases {
		errs := Validate(s, jsonAny(t, v))
		if (len(errs) > 0) != wantErr {
			t.Errorf("%s: got errs=%v wantErr=%v", v, errs, wantErr)
		}
	}
}

func TestValidate_Object_Required(t *testing.T) {
	s := &Schema{
		Type:     "object",
		Required: []string{"name", "age"},
		Properties: map[string]*Schema{
			"name": {Type: "string"},
			"age":  {Type: "integer"},
		},
	}

	errs := Validate(s, jsonAny(t, `{"name":"Casey","age":42}`))
	if len(errs) != 0 {
		t.Errorf("complete object: unexpected errs %v", errs)
	}

	errs = Validate(s, jsonAny(t, `{"name":"Casey"}`))
	if len(errs) != 1 || errs[0].Path != "/age" {
		t.Errorf("missing age: got errs=%v want one error at /age", errs)
	}

	errs = Validate(s, jsonAny(t, `{}`))
	if len(errs) != 2 {
		t.Errorf("empty object: got %d errs (%v) want 2", len(errs), errs)
	}
}

func TestValidate_Object_AdditionalPropertiesFalse(t *testing.T) {
	s := &Schema{
		Type:                 "object",
		Properties:           map[string]*Schema{"a": {Type: "string"}},
		AdditionalProperties: false,
	}

	if errs := Validate(s, jsonAny(t, `{"a":"hi"}`)); len(errs) != 0 {
		t.Errorf("declared-only: unexpected %v", errs)
	}

	errs := Validate(s, jsonAny(t, `{"a":"hi","b":1}`))
	if len(errs) != 1 || errs[0].Path != "/b" {
		t.Errorf("extra prop: got %v want one error at /b", errs)
	}
}

func TestValidate_Object_NestedErrorPaths(t *testing.T) {
	s := &Schema{
		Type: "object",
		Properties: map[string]*Schema{
			"inner": {
				Type: "object",
				Properties: map[string]*Schema{
					"n": {Type: "integer", Minimum: fl(0)},
				},
			},
		},
	}
	errs := Validate(s, jsonAny(t, `{"inner":{"n":-5}}`))
	if len(errs) != 1 || errs[0].Path != "/inner/n" {
		t.Errorf("nested path: got %v want /inner/n", errs)
	}
}

func TestValidate_Array_Items(t *testing.T) {
	s := &Schema{
		Type:  "array",
		Items: &Schema{Type: "integer", Minimum: fl(0)},
	}

	if errs := Validate(s, jsonAny(t, `[1,2,3]`)); len(errs) != 0 {
		t.Errorf("valid array: unexpected %v", errs)
	}

	errs := Validate(s, jsonAny(t, `[1,-2,3,-4]`))
	if len(errs) != 2 {
		t.Errorf("two bad items: got %d errs %v want 2", len(errs), errs)
	}
	if errs[0].Path != "/1" || errs[1].Path != "/3" {
		t.Errorf("array paths: got %v want /1 then /3", errs)
	}
}

func TestValidate_Array_NestedObjects(t *testing.T) {
	s := &Schema{
		Type: "array",
		Items: &Schema{
			Type:     "object",
			Required: []string{"k"},
			Properties: map[string]*Schema{
				"k": {Type: "string"},
				"v": {Type: "integer", Minimum: fl(1), Maximum: fl(5)},
			},
		},
	}
	errs := Validate(s, jsonAny(t, `[{"k":"a","v":3},{"v":99}]`))
	// Expected: missing k at /1/k, v out of range at /1/v
	if len(errs) != 2 {
		t.Fatalf("got %d errs (%v) want 2", len(errs), errs)
	}
	gotPaths := map[string]bool{errs[0].Path: true, errs[1].Path: true}
	if !gotPaths["/1/k"] || !gotPaths["/1/v"] {
		t.Errorf("array-of-objects paths: got %v", errs)
	}
}

func TestValidate_PtrEnc_SpecialChars(t *testing.T) {
	s := &Schema{
		Type:     "object",
		Required: []string{"a/b", "c~d"},
		Properties: map[string]*Schema{
			"a/b": {Type: "string"},
			"c~d": {Type: "string"},
		},
	}
	errs := Validate(s, jsonAny(t, `{}`))
	if len(errs) != 2 {
		t.Fatalf("got %d errs %v", len(errs), errs)
	}
	gotPaths := map[string]bool{errs[0].Path: true, errs[1].Path: true}
	// Per RFC 6901: '/' → '~1', '~' → '~0'.
	if !gotPaths["/a~1b"] {
		t.Errorf("expected /a~1b in %v", errs)
	}
	if !gotPaths["/c~0d"] {
		t.Errorf("expected /c~0d in %v", errs)
	}
}

func TestValidate_NilSchemaIsNoOp(t *testing.T) {
	if errs := Validate(nil, "anything"); errs != nil {
		t.Errorf("nil schema returned errs: %v", errs)
	}
}
