package exec

import (
	"encoding/json"
	"testing"

	"github.com/stretchr/testify/require"
)

func TestCompactProjectionSchemaReusesRepeatedObjects(t *testing.T) {
	raw := json.RawMessage(`{
		"$schema":"https://json-schema.org/draft/2020-12/schema",
		"type":"object",
		"properties":{
			"current":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id"],"additionalProperties":false},
			"proposed":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id"],"additionalProperties":false}
		},
		"additionalProperties":false
	}`)

	projected, err := compactProjectionSchema(raw)
	require.NoError(t, err)
	require.Less(t, len(projected), len(raw))
	require.Contains(t, string(projected), `"$defs"`)
	require.Contains(t, string(projected), `"$ref"`)
	_, err = compileSchema(projected)
	require.NoError(t, err)
}

func TestCompactProjectionSchemaLeavesUniqueSmallSchemaAlone(t *testing.T) {
	raw := json.RawMessage(`{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"id":{"type":"string"}},"additionalProperties":false}`)
	projected, err := compactProjectionSchema(raw)
	require.NoError(t, err)
	require.NotContains(t, string(projected), `"$defs"`)
	_, err = compileSchema(projected)
	require.NoError(t, err)
}

func TestCompactOutputSchemaPreservesNestedObjectClosure(t *testing.T) {
	raw := json.RawMessage(`{
		"type":"object",
		"properties":{
			"data":{
				"type":"object",
				"properties":{"task":{"type":"string"}},
				"required":["task"],
				"additionalProperties":false
			}
		},
		"required":["data"],
		"additionalProperties":false
	}`)

	projected := compactModelSchema(raw, true)
	projected, err := compactProjectionSchema(projected)
	require.NoError(t, err)
	compiled := mustCompile(t, projected)
	require.NoError(t, validateInstance(compiled, json.RawMessage(`{"data":{"task":"ok"}}`)))
	require.Error(t, validateInstance(compiled, json.RawMessage(`{"data":{"task":"ok","unknown":true}}`)))
}

// A property whose name collides with a JSON Schema annotation keyword
// (description/title/default) must survive model-facing compaction. Only the
// annotation forms (siblings of type/properties on a schema node) are stripped.
func TestCompactModelSchemaPreservesPropertyNamedAfterAnnotationKeywords(t *testing.T) {
	raw := json.RawMessage(`{
		"type":"object",
		"properties":{
			"description":{"type":"string"},
			"title":{"type":"string"},
			"default":{"type":"integer"},
			"name":{"type":"string","description":"display name","title":"named","default":"x","examples":["a"]}
		},
		"required":["description","title"],
		"additionalProperties":false
	}`)

	compacted := compactModelSchema(raw, true)

	var doc map[string]any
	require.NoError(t, json.Unmarshal(compacted, &doc))

	props, ok := doc["properties"].(map[string]any)
	require.True(t, ok, "properties container preserved")
	for _, field := range []string{"description", "title", "default", "name"} {
		_, present := props[field]
		require.True(t, present, "property %q must survive compaction", field)
	}

	required, _ := doc["required"].([]any)
	require.Contains(t, required, "description")

	// Annotation forms on the `name` subschema are still stripped.
	nameSchema, ok := props["name"].(map[string]any)
	require.True(t, ok)
	for _, annotation := range []string{"description", "title", "default", "examples"} {
		_, present := nameSchema[annotation]
		require.False(t, present, "annotation %q on name subschema must be stripped", annotation)
	}
	require.Equal(t, "string", nameSchema["type"])
	require.Equal(t, false, doc["additionalProperties"])

	// Behavior is unchanged after projection.
	projected, err := compactProjectionSchema(compacted)
	require.NoError(t, err)
	compiled := mustCompile(t, projected)
	require.NoError(t, validateInstance(compiled, json.RawMessage(`{"description":"d","title":"t"}`)))
	require.Error(t, validateInstance(compiled, json.RawMessage(`{"title":"t"}`)))                               // missing required description
	require.Error(t, validateInstance(compiled, json.RawMessage(`{"description":"d","title":"t","unknown":1}`))) // additionalProperties:false
}

// Data-valued keywords (const/enum/default/examples) must never be descended
// into: doing so would delete keys named after annotations and corrupt literal
// constant/default data.
func TestCompactModelSchemaPreservesConstEnumData(t *testing.T) {
	raw := json.RawMessage(`{
		"type":"object",
		"properties":{
			"kind":{"type":"object","const":{"title":"k","description":"d","default":"x","name":"n"}},
			"shapes":{"type":"object","enum":[{"title":"a","description":"b"},{"name":"c"}]}
		},
		"additionalProperties":false
	}`)

	compacted := compactModelSchema(raw, true)

	var doc map[string]any
	require.NoError(t, json.Unmarshal(compacted, &doc))
	props := doc["properties"].(map[string]any)

	kind := props["kind"].(map[string]any)
	require.Equal(t, map[string]any{
		"title":       "k",
		"description": "d",
		"default":     "x",
		"name":        "n",
	}, kind["const"], "const data value preserved verbatim")

	shapes := props["shapes"].(map[string]any)
	require.Equal(t, []any{
		map[string]any{"title": "a", "description": "b"},
		map[string]any{"name": "c"},
	}, shapes["enum"], "enum data value preserved verbatim")
}

// Annotations are stripped and const data preserved across every schema-valued
// position (oneOf branch, $defs entry, items), proving the walker recurses only
// along the Draft 2020-12 keyword groups and never enters data values there.
func TestCompactModelSchemaAcrossSubschemaPositions(t *testing.T) {
	raw := json.RawMessage(`{
		"oneOf":[
			{"type":"object","properties":{"description":{"type":"string"}},"const":{"title":"kept"}},
			{"type":"object","title":"branch doc","properties":{"name":{"type":"string"}}}
		],
		"$defs":{"Shape":{"type":"object","description":"def doc","properties":{"title":{"type":"string"}}}},
		"items":{"type":"object","description":"item doc","const":{"default":"x","description":"d"}}
	}`)

	out := compactModelSchema(raw, true)
	var doc map[string]any
	require.NoError(t, json.Unmarshal(out, &doc))

	branch0 := doc["oneOf"].([]any)[0].(map[string]any)
	require.Equal(t, map[string]any{"title": "kept"}, branch0["const"], "const data in oneOf branch preserved")
	require.Contains(t, branch0["properties"].(map[string]any), "description", "business property in oneOf branch preserved")

	branch1 := doc["oneOf"].([]any)[1].(map[string]any)
	_, hasBranchTitle := branch1["title"]
	require.False(t, hasBranchTitle, "annotation title stripped from oneOf branch")

	shape := doc["$defs"].(map[string]any)["Shape"].(map[string]any)
	_, hasDefDescription := shape["description"]
	require.False(t, hasDefDescription, "annotation description stripped from $defs entry")
	require.Contains(t, shape["properties"].(map[string]any), "title", "business property title in $defs preserved")

	items := doc["items"].(map[string]any)
	require.Equal(t, map[string]any{"default": "x", "description": "d"}, items["const"], "const data in items preserved")
	_, hasItemDescription := items["description"]
	require.False(t, hasItemDescription, "annotation description stripped from items subschema")
}

// Walker coverage for the remaining Draft 2020 schema-valued keyword groups:
// patternProperties and dependentSchemas (named maps), if/then/else (single
// subschemas), and prefixItems (array of subschemas). Annotations inside are
// stripped, business properties named after annotations survive, and const data
// is preserved — the walker descends only along schema-valued positions.
func TestCompactModelSchemaAcrossRemainingSubschemaKeywords(t *testing.T) {
	raw := json.RawMessage(`{
		"patternProperties":{"^x":{"type":"object","description":"pp doc","properties":{"title":{"type":"string"}}}},
		"dependentSchemas":{"k":{"type":"object","description":"ds doc","const":{"title":"kept"}}},
		"if":{"type":"object","description":"if doc","properties":{"title":{"type":"string"}}},
		"then":{"type":"object","description":"then doc"},
		"prefixItems":[{"type":"object","description":"pi doc","const":{"default":"d"}}]
	}`)
	out := compactModelSchema(raw, true)
	var doc map[string]any
	require.NoError(t, json.Unmarshal(out, &doc))

	pp := doc["patternProperties"].(map[string]any)["^x"].(map[string]any)
	_, ppDescription := pp["description"]
	require.False(t, ppDescription, "annotation stripped from patternProperties subschema")
	require.Contains(t, pp["properties"].(map[string]any), "title", "business title in patternProperties preserved")

	ds := doc["dependentSchemas"].(map[string]any)["k"].(map[string]any)
	_, dsDescription := ds["description"]
	require.False(t, dsDescription, "annotation stripped from dependentSchemas subschema")
	require.Equal(t, map[string]any{"title": "kept"}, ds["const"], "const data in dependentSchemas preserved")

	ifSub := doc["if"].(map[string]any)
	_, ifDescription := ifSub["description"]
	require.False(t, ifDescription, "annotation stripped from if subschema")
	require.Contains(t, ifSub["properties"].(map[string]any), "title", "business title in if subschema preserved")

	thenSub := doc["then"].(map[string]any)
	_, thenDescription := thenSub["description"]
	require.False(t, thenDescription, "annotation stripped from then subschema")

	pi := doc["prefixItems"].([]any)[0].(map[string]any)
	_, piDescription := pi["description"]
	require.False(t, piDescription, "annotation stripped from prefixItems subschema")
	require.Equal(t, map[string]any{"default": "d"}, pi["const"], "const data in prefixItems preserved")
}
