/*
 * Copyright 2026-2026 the original author or authors.
 */

package io.modelcontextprotocol.json;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import io.modelcontextprotocol.spec.McpSchema;

import io.modelcontextprotocol.json.schema.jackson3.DefaultJsonSchemaValidator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import tools.jackson.core.type.TypeReference;
import tools.jackson.databind.json.JsonMapper;

import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;

/**
 * Tests for {@link DefaultJsonSchemaValidator}.
 *
 * @author Filip Hrisafov
 */
class DefaultJsonSchemaValidatorTests {

	private DefaultJsonSchemaValidator validator;

	private JsonMapper jsonMapper;

	@Mock
	private JsonMapper mockJsonMapper;

	@BeforeEach
	void setUp() {
		MockitoAnnotations.openMocks(this);
		validator = new DefaultJsonSchemaValidator();
		jsonMapper = JsonMapper.shared();
	}

	/**
	 * Utility method to convert JSON string to Map<String, Object>
	 */
	private Map<String, Object> toMap(String json) {
		try {
			return jsonMapper.readValue(json, new TypeReference<>() {
			});
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to parse JSON: " + json, e);
		}
	}

	private List<Map<String, Object>> toListMap(String json) {
		try {
			return jsonMapper.readValue(json, new TypeReference<>() {
			});
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to parse JSON: " + json, e);
		}
	}

	@Test
	void testDefaultConstructor() {
		DefaultJsonSchemaValidator defaultValidator = new DefaultJsonSchemaValidator();

		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"test": {"type": "string"}
					}
				}
				""";
		String contentJson = """
				{
					"test": "value"
				}
				""";

		ValidationResponse response = defaultValidator.validate(toMap(schemaJson), toMap(contentJson));
		assertTrue(response.valid());
	}

	@Test
	void testConstructorWithObjectMapper() {
		JsonMapper customMapper = JsonMapper.builder().build();
		DefaultJsonSchemaValidator customValidator = new DefaultJsonSchemaValidator(customMapper);

		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"test": {"type": "string"}
					}
				}
				""";
		String contentJson = """
				{
					"test": "value"
				}
				""";

		ValidationResponse response = customValidator.validate(toMap(schemaJson), toMap(contentJson));
		assertTrue(response.valid());
	}

	@Test
	void testValidateWithValidStringSchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"},
						"age": {"type": "integer"}
					},
					"required": ["name", "age"]
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"age": 30
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
		assertNotNull(response.jsonStructuredOutput());
	}

	@Test
	void testValidateWithValidNumberSchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"price": {"type": "number", "minimum": 0},
						"quantity": {"type": "integer", "minimum": 1}
					},
					"required": ["price", "quantity"]
				}
				""";

		String contentJson = """
				{
					"price": 19.99,
					"quantity": 5
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithValidArraySchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"items": {
							"type": "array",
							"items": {"type": "string"}
						}
					},
					"required": ["items"]
				}
				""";

		String contentJson = """
				{
					"items": ["apple", "banana", "cherry"]
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithValidArraySchemaTopLevelArray() {
		String schemaJson = """
				{
					"$schema" : "https://json-schema.org/draft/2020-12/schema",
					"type" : "array",
					"items" : {
						"type" : "object",
						"properties" : {
						"city" : {
							"type" : "string"
						},
						"summary" : {
							"type" : "string"
						},
						"temperatureC" : {
							"type" : "number",
							"format" : "float"
						}
						},
						"required" : [ "city", "summary", "temperatureC" ]
					},
					"additionalProperties" : false
				}
					""";

		String contentJson = """
				[
					{
						"city": "London",
						"summary": "Generally mild with frequent rainfall. Winters are cool and damp, summers are warm but rarely hot. Cloudy conditions are common throughout the year.",
						"temperatureC": 11.3
					},
					{
						"city": "New York",
						"summary": "Four distinct seasons with hot and humid summers, cold winters with snow, and mild springs and autumns. Precipitation is fairly evenly distributed throughout the year.",
						"temperatureC": 12.8
					},
					{
						"city": "San Francisco",
						"summary": "Mild year-round with a distinctive Mediterranean climate. Famous for summer fog, mild winters, and little temperature variation throughout the year. Very little rainfall in summer months.",
						"temperatureC": 14.6
					},
					{
						"city": "Tokyo",
						"summary": "Humid subtropical climate with hot, wet summers and mild winters. Experiences a rainy season in early summer and occasional typhoons in late summer to early autumn.",
						"temperatureC": 15.4
					}
				]
				""";

		Map<String, Object> schema = toMap(schemaJson);

		// Validate as JSON string
		ValidationResponse response = validator.validate(schema, contentJson);

		assertTrue(response.valid());
		assertNull(response.errorMessage());

		List<Map<String, Object>> structuredContent = toListMap(contentJson);

		// Validate as List<Map<String, Object>>
		response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithInvalidTypeSchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"},
						"age": {"type": "integer"}
					},
					"required": ["name", "age"]
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"age": "thirty"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
		assertTrue(response.errorMessage().contains("JSON schema validation errors"));
	}

	@Test
	void testValidateWithMissingRequiredField() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"},
						"age": {"type": "integer"}
					},
					"required": ["name", "age"]
				}
				""";

		String contentJson = """
				{
					"name": "John Doe"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
	}

	@Test
	void testValidateWithAdditionalPropertiesNotAllowed() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"}
					},
					"required": ["name"],
					"additionalProperties": false
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"extraField": "should not be allowed"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
	}

	@Test
	void testValidateWithAdditionalPropertiesExplicitlyAllowed() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"}
					},
					"required": ["name"],
					"additionalProperties": true
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"extraField": "should be allowed"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithDefaultAdditionalProperties() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"}
					},
					"required": ["name"],
					"additionalProperties": true
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"extraField": "should be allowed"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithAdditionalPropertiesExplicitlyDisallowed() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"name": {"type": "string"}
					},
					"required": ["name"],
					"additionalProperties": false
				}
				""";

		String contentJson = """
				{
					"name": "John Doe",
					"extraField": "should not be allowed"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
	}

	@Test
	void testValidateWithEmptySchema() {
		String schemaJson = """
				{
					"additionalProperties": true
				}
				""";

		String contentJson = """
				{
					"anything": "goes"
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithEmptyContent() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {}
				}
				""";

		String contentJson = """
				{}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithNestedObjectSchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"person": {
							"type": "object",
							"properties": {
								"name": {"type": "string"},
								"address": {
									"type": "object",
									"properties": {
										"street": {"type": "string"},
										"city": {"type": "string"}
									},
									"required": ["street", "city"]
								}
							},
							"required": ["name", "address"]
						}
					},
					"required": ["person"]
				}
				""";

		String contentJson = """
				{
					"person": {
						"name": "John Doe",
						"address": {
							"street": "123 Main St",
							"city": "Anytown"
						}
					}
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertTrue(response.valid());
		assertNull(response.errorMessage());
	}

	@Test
	void testValidateWithInvalidNestedObjectSchema() {
		String schemaJson = """
				{
					"type": "object",
					"properties": {
						"person": {
							"type": "object",
							"properties": {
								"name": {"type": "string"},
								"address": {
									"type": "object",
									"properties": {
										"street": {"type": "string"},
										"city": {"type": "string"}
									},
									"required": ["street", "city"]
								}
							},
							"required": ["name", "address"]
						}
					},
					"required": ["person"]
				}
				""";

		String contentJson = """
				{
					"person": {
						"name": "John Doe",
						"address": {
							"street": "123 Main St"
						}
					}
				}
				""";

		Map<String, Object> schema = toMap(schemaJson);
		Map<String, Object> structuredContent = toMap(contentJson);

		ValidationResponse response = validator.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
	}

	@Test
	void testValidateWithJsonProcessingException() {
		DefaultJsonSchemaValidator validatorWithMockMapper = new DefaultJsonSchemaValidator(mockJsonMapper);

		Map<String, Object> schema = Map.of("type", "object");
		Map<String, Object> structuredContent = Map.of("key", "value");

		// This will trigger our null check and throw JsonProcessingException
		when(mockJsonMapper.valueToTree(any())).thenReturn(null);

		ValidationResponse response = validatorWithMockMapper.validate(schema, structuredContent);

		assertFalse(response.valid());
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Error parsing tool JSON Schema"));
		assertTrue(response.errorMessage().contains("Failed to convert schema to JsonNode"));
	}

	@ParameterizedTest
	@MethodSource("provideValidSchemaAndContentPairs")
	void testValidateWithVariousValidInputs(Map<String, Object> schema, Map<String, Object> content) {
		ValidationResponse response = validator.validate(schema, content);

		assertTrue(response.valid(), "Expected validation to pass for schema: " + schema + " and content: " + content);
		assertNull(response.errorMessage());
	}

	@ParameterizedTest
	@MethodSource("provideInvalidSchemaAndContentPairs")
	void testValidateWithVariousInvalidInputs(Map<String, Object> schema, Map<String, Object> content) {
		ValidationResponse response = validator.validate(schema, content);

		assertFalse(response.valid(), "Expected validation to fail for schema: " + schema + " and content: " + content);
		assertNotNull(response.errorMessage());
		assertTrue(response.errorMessage().contains("Validation failed"));
	}

	private static Map<String, Object> staticToMap(String json) {
		try {
			return JsonMapper.shared().readValue(json, new TypeReference<>() {
			});
		}
		catch (Exception e) {
			throw new RuntimeException("Failed to parse JSON: " + json, e);
		}
	}

	private static Stream<Arguments> provideValidSchemaAndContentPairs() {
		return Stream.of(
				// Boolean schema
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"flag": {"type": "boolean"}
							}
						}
						"""), staticToMap("""
						{
							"flag": true
						}
						""")),
				// String with additional properties allowed
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"name": {"type": "string"}
							},
							"additionalProperties": true
						}
						"""), staticToMap("""
						{
							"name": "test",
							"extra": "allowed"
						}
						""")),
				// Array with specific items
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"numbers": {
									"type": "array",
									"items": {"type": "number"}
								}
							}
						}
						"""), staticToMap("""
						{
							"numbers": [1.0, 2.5, 3.14]
						}
						""")),
				// Enum validation
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"status": {
									"type": "string",
									"enum": ["active", "inactive", "pending"]
								}
							}
						}
						"""), staticToMap("""
						{
							"status": "active"
						}
						""")));
	}

	private static Stream<Arguments> provideInvalidSchemaAndContentPairs() {
		return Stream.of(
				// Wrong boolean type
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"flag": {"type": "boolean"}
							}
						}
						"""), staticToMap("""
						{
							"flag": "true"
						}
						""")),
				// Array with wrong item types
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"numbers": {
									"type": "array",
									"items": {"type": "number"}
								}
							}
						}
						"""), staticToMap("""
						{
							"numbers": ["one", "two", "three"]
						}
						""")),
				// Invalid enum value
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"status": {
									"type": "string",
									"enum": ["active", "inactive", "pending"]
								}
							}
						}
						"""), staticToMap("""
						{
							"status": "unknown"
						}
						""")),
				// Minimum constraint violation
				Arguments.of(staticToMap("""
						{
							"type": "object",
							"properties": {
								"age": {"type": "integer", "minimum": 0}
							}
						}
						"""), staticToMap("""
						{
							"age": -5
						}
						""")));
	}

	@Test
	void testValidationResponseToValid() {
		String jsonOutput = "{\"test\":\"value\"}";
		ValidationResponse response = ValidationResponse.asValid(jsonOutput);
		assertTrue(response.valid());
		assertNull(response.errorMessage());
		assertEquals(jsonOutput, response.jsonStructuredOutput());
	}

	@Test
	void testValidationResponseToInvalid() {
		String errorMessage = "Test error message";
		ValidationResponse response = ValidationResponse.asInvalid(errorMessage);
		assertFalse(response.valid());
		assertEquals(errorMessage, response.errorMessage());
		assertNull(response.jsonStructuredOutput());
	}

	@Test
	void testValidationResponseRecord() {
		ValidationResponse response1 = new ValidationResponse(true, null, "{\"valid\":true}");
		ValidationResponse response2 = new ValidationResponse(false, "Error", null);

		assertTrue(response1.valid());
		assertNull(response1.errorMessage());
		assertEquals("{\"valid\":true}", response1.jsonStructuredOutput());

		assertFalse(response2.valid());
		assertEquals("Error", response2.errorMessage());
		assertNull(response2.jsonStructuredOutput());

		// Test equality
		ValidationResponse response3 = new ValidationResponse(true, null, "{\"valid\":true}");
		assertEquals(response1, response3);
		assertNotEquals(response1, response2);
	}

	@Test
	void validatesSchemaWithExplicitDraft07Dialect() {
		Map<String, Object> schema = Map.of("$schema", "http://json-schema.org/draft-07/schema#", "type", "object",
				"properties", Map.of("name", Map.of("type", "string")), "required", List.of("name"));

		assertTrue(validator.validate(schema, Map.of("name", "alice")).valid());
		assertFalse(validator.validate(schema, Map.of()).valid());
	}

	@Test
	void validatesSchemaWithExplicit2020_12Dialect() {
		Map<String, Object> schema = Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12, "type", "object",
				"properties", Map.of("name", Map.of("type", "string")), "required", List.of("name"));

		assertTrue(validator.validate(schema, Map.of("name", "alice")).valid());
		assertFalse(validator.validate(schema, Map.of()).valid());
	}

	@Test
	void validatesSchemaWith2020_12Keywords() {
		Map<String, Object> schema = Map.of("type", "array", "prefixItems",
				List.of(Map.of("type", "string"), Map.of("type", "number")));

		assertTrue(validator.validate(schema, List.of("hello", 42)).valid());
		assertFalse(validator.validate(schema, List.of(1, "wrong")).valid());
	}

	@Test
	void validatesOutputAgainstSchemaWithDefsAndRef() {
		Map<String, Object> schema = Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12, "type", "object", "$defs",
				Map.of("address",
						Map.of("type", "object", "properties",
								Map.of("street", Map.of("type", "string"), "city", Map.of("type", "string")))),
				"properties", Map.of("name", Map.of("type", "string"), "address", Map.of("$ref", "#/$defs/address")),
				"additionalProperties", false);

		assertTrue(validator
			.validate(schema, Map.of("name", "alice", "address", Map.of("street", "1 Main", "city", "Springfield")))
			.valid());
		assertFalse(validator.validate(schema, Map.of("name", "alice", "extra", 1)).valid());
	}

	@Test
	void validateSchemaAcceptsValidSchema() {
		Map<String, Object> schema = Map.of("type", "object", "properties",
				Map.of("name", Map.of("type", "string"), "age", Map.of("type", "integer")), "required",
				List.of("name"));

		assertTrue(validator.validateSchema(schema).valid());
	}

	@Test
	void validateSchemaAcceptsValid2020_12SchemaWithExplicitDialect() {
		Map<String, Object> schema = Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12, "type", "object",
				"properties", Map.of("count", Map.of("type", "integer")));

		assertTrue(validator.validateSchema(schema).valid());
	}

	@Test
	void validateSchemaRejectsSchemaWithInvalidTypeValue() {
		Map<String, Object> schema = Map.of("type", "not-a-valid-type");

		assertFalse(validator.validateSchema(schema).valid());
	}

	@Test
	void validateSchemaRejectsSchemaWithWrongTypeForRequired() {
		Map<String, Object> schema = Map.of("type", "object", "required", "should-be-an-array");

		assertFalse(validator.validateSchema(schema).valid());
	}

	@Test
	void validateSchemaSkipsDraft07SchemasWithExplicitDialect() {
		Map<String, Object> schema = Map.of("$schema", "http://json-schema.org/draft-07/schema#", "type", "object",
				"properties", Map.of("a", Map.of("type", "string")));

		assertTrue(validator.validateSchema(schema).valid());
	}

	@Test
	void assertConformsDoesNothingOnNullSchema() {
		validator.assertConforms("test context", null);
	}

	@Test
	void assertConformsPassesForValidSchema() {
		Map<String, Object> schema = Map.of("type", "object", "properties", Map.of("name", Map.of("type", "string")));

		validator.assertConforms("Tool 'my-tool' inputSchema", schema);
	}

	@Test
	void assertConformsThrowsForInvalidSchema() {
		Map<String, Object> schema = Map.of("type", "not-a-valid-type");

		assertThatThrownBy(() -> validator.assertConforms("Tool 'bad' inputSchema", schema))
			.isInstanceOf(IllegalArgumentException.class)
			.hasMessageContaining("Tool 'bad' inputSchema")
			.hasMessageContaining("SEP-1613");
	}

}
