#![allow(clippy::large_stack_arrays, clippy::needless_pass_by_value)]

mod tests {
    use jsonschema::{canonical::CanonicalKind, Draft, PatternOptions, Registry};
    #[cfg(not(target_arch = "wasm32"))]
    use std::env;
    #[cfg(not(target_arch = "wasm32"))]
    use std::fs;
    use testsuite::{suite, CodegenValidator, Test};

    #[suite(
    path = "crates/jsonschema/tests/suite",
    drafts = [
        "draft4",
        "draft6",
        "draft7",
        "draft2019-09",
        "draft2020-12",
    ],
    xfail = [
        "draft4::optional::bignum::integer::a_bignum_is_an_integer",
        "draft4::optional::bignum::integer::a_negative_bignum_is_an_integer",
        // Lone surrogate: unrepresentable as a Rust string; the loader rewrites it to
        // U+FFFD, a valid idn-email local part, so the rejection can't be exercised.
        "draft7::optional::format::idn_email::validation_of_an_internationalized_e_mail_addresses::a_local_part_with_a_lone_utf_16_surrogate_is_invalid",
        "draft2019-09::optional::format::idn_email::validation_of_an_internationalized_e_mail_addresses::a_local_part_with_a_lone_utf_16_surrogate_is_invalid",
        "draft2020-12::optional::format::idn_email::validation_of_an_internationalized_e_mail_addresses::a_local_part_with_a_lone_utf_16_surrogate_is_invalid",
    ]
)]
    fn test_suite(test: &Test, codegen_validator: Box<dyn CodegenValidator>) {
        enum RegexEngine {
            Regex,
            FancyRegex,
        }

        let draft = match test.draft {
            "draft4" => Draft::Draft4,
            "draft6" => Draft::Draft6,
            "draft7" => Draft::Draft7,
            "draft2019-09" => Draft::Draft201909,
            "draft2020-12" => Draft::Draft202012,
            _ => panic!("Unsupported draft"),
        };
        if should_skip_draft(test.draft) {
            return;
        }
        let root_uri = test.schema[draft.id_keyword()]
            .as_str()
            .unwrap_or("json-schema:///");
        let registry = Registry::new()
            .retriever(testsuite_retriever())
            .draft(draft)
            .add(root_uri, &test.schema)
            .expect("Failed to add the test schema to the registry")
            .prepare()
            .expect("Failed to prepare the test schema registry");
        let mut options = jsonschema::options()
            .with_draft(draft)
            .with_registry(&registry);
        if test.is_optional {
            options = options.should_validate_formats(true);
        }

        for engine in [RegexEngine::FancyRegex, RegexEngine::Regex] {
            match engine {
                RegexEngine::Regex => {
                    options = options.with_pattern_options(PatternOptions::regex());
                }
                RegexEngine::FancyRegex => {
                    options = options.with_pattern_options(PatternOptions::fancy_regex());
                }
            }
            let validator = options
                .build(&test.schema)
                .expect("Failed to build a schema");

            // Canonicalization must not change what a schema accepts. A `Raw` document round-trips
            // verbatim, so only the modeled subset is worth comparing.
            let mut canonicalize_options = jsonschema::canonical::options()
                .with_draft(draft)
                .with_registry(&registry);
            match engine {
                RegexEngine::Regex => {
                    canonicalize_options =
                        canonicalize_options.with_pattern_options(PatternOptions::regex());
                }
                RegexEngine::FancyRegex => {
                    canonicalize_options =
                        canonicalize_options.with_pattern_options(PatternOptions::fancy_regex());
                }
            }
            if test.is_optional {
                canonicalize_options = canonicalize_options.should_validate_formats(true);
            }
            // Unmodeled constructs come back as `Raw`, never as an error, so any error is a regression.
            let canonical = canonicalize_options
                .canonicalize(&test.schema)
                .unwrap_or_else(|error| {
                    panic!(
                        "Canonicalization failed:\nCase: {}\nTest: {}\nSchema: {}\nError: {error}",
                        test.case,
                        test.description,
                        pretty_json(&test.schema),
                    )
                });
            if canonical.kind() != CanonicalKind::Raw {
                let canonical_value = canonical.to_json_schema();
                let mut canonical_options = jsonschema::options()
                    .with_draft(canonical.draft())
                    .with_retriever(testsuite_retriever());
                if test.is_optional {
                    canonical_options = canonical_options.should_validate_formats(true);
                }
                match engine {
                    RegexEngine::Regex => {
                        canonical_options =
                            canonical_options.with_pattern_options(PatternOptions::regex());
                    }
                    RegexEngine::FancyRegex => {
                        canonical_options =
                            canonical_options.with_pattern_options(PatternOptions::fancy_regex());
                    }
                }
                let canonical_validator = canonical_options
                    .build(&canonical_value)
                    .unwrap_or_else(|error| {
                        panic!(
                            "Canonical schema failed to compile:\nCase: {}\nTest: {}\nSchema: {}\nCanonical: {}\nError: {error}",
                            test.case,
                            test.description,
                            pretty_json(&test.schema),
                            pretty_json(&canonical_value),
                        )
                    });
                assert_eq!(
                    validator.is_valid(&test.data),
                    canonical_validator.is_valid(&test.data),
                    "Canonicalization changed validation:\nCase: {}\nTest: {}\nSchema: {}\nCanonical: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&canonical_value),
                    pretty_json(&test.data),
                );
            }

            if test.valid {
                if let Some(first) = validator.iter_errors(&test.data).next() {
                    panic!(
                    "Test case should not have validation errors:\nGroup: {}\nTest case: {}\nSchema: {}\nInstance: {}\nError: {first:?}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                }
                assert!(
                    validator.is_valid(&test.data),
                    "Test case should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                assert!(
                    codegen_validator.is_valid(&test.data),
                    "Test case should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                assert!(
                    codegen_validator.validate(&test.data).is_ok(),
                    "Test case should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                assert!(
                    validator.validate(&test.data).is_ok(),
                    "Test case should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                let evaluation = validator.evaluate(&test.data);
                assert!(
                    evaluation.flag().valid,
                    "Evaluation output should be valid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                let _ =
                    serde_json::to_value(evaluation.list()).expect("List output should serialize");
                let _ = serde_json::to_value(evaluation.hierarchical())
                    .expect("Hierarchical output should serialize");
            } else {
                let mut errors = validator.iter_errors(&test.data);
                let Some(first_error) = errors.next() else {
                    panic!(
                        "Test case should have validation errors:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                        test.case,
                        test.description,
                        pretty_json(&test.schema),
                        pretty_json(&test.data),
                    );
                };

                let pointer = first_error.instance_path().as_str();
                assert_eq!(
                    test.data.pointer(pointer),
                    Some(first_error.instance().as_ref()),
                    "Expected error instance did not match actual error instance:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                    first_error.instance().as_ref(),
                    pointer,
                );

                let first_error_parts = first_error.into_parts();
                let evaluation_path = first_error_parts.evaluation_path;
                assert!(
                    evaluation_path.as_str().is_empty()
                        || evaluation_path.as_str().starts_with('/'),
                    "Evaluation path should be a JSON pointer: {evaluation_path}"
                );

                for error in errors {
                    let pointer = error.instance_path().as_str();
                    assert_eq!(
                    test.data.pointer(pointer), Some(error.instance().as_ref()),
                    "Expected error instance did not match actual error instance:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                    error.instance().as_ref(),
                    pointer,
                );
                }
                assert!(
                    !validator.is_valid(&test.data),
                    "Test case should be invalid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                assert!(
                    !codegen_validator.is_valid(&test.data),
                    "Test case should be invalid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                let Some(error) = validator.validate(&test.data).err() else {
                    panic!(
                    "Test case should be invalid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                };
                let pointer = error.instance_path().as_str();
                assert_eq!(
                test.data.pointer(pointer), Some(error.instance().as_ref()),
                "Expected error instance did not match actual error instance:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}\nExpected pointer: {:#?}\nActual pointer: {:#?}",
                test.case,
                test.description,
                pretty_json(&test.schema),
                pretty_json(&test.data),
                error.instance().as_ref(),
                pointer,
            );
                let runtime_message = error.to_string();
                let error_parts = error.into_parts();
                let evaluation_path = error_parts.evaluation_path;
                assert!(
                    evaluation_path.as_str().is_empty()
                        || evaluation_path.as_str().starts_with('/'),
                    "Evaluation path should be a JSON pointer: {evaluation_path}"
                );

                // Compare codegen validate() error against runtime validate()
                let codegen_error = codegen_validator
                    .validate(&test.data)
                    .err()
                    .unwrap_or_else(|| panic!(
                        "codegen validate() should return Err:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                        test.case, test.description,
                        pretty_json(&test.schema), pretty_json(&test.data),
                    ));
                let codegen_validation_error = codegen_error
                    .downcast::<jsonschema::ValidationError<'static>>()
                    .expect("codegen validate() error should downcast to ValidationError");
                let codegen_message = codegen_validation_error.to_string();
                let codegen_parts = (*codegen_validation_error).into_parts();
                assert_eq!(
                    codegen_parts.schema_path, error_parts.schema_path,
                    "codegen validate() schema_path mismatch:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case, test.description,
                    pretty_json(&test.schema), pretty_json(&test.data),
                );
                assert_eq!(
                    codegen_parts.instance_path, error_parts.instance_path,
                    "codegen validate() instance_path mismatch:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case, test.description,
                    pretty_json(&test.schema), pretty_json(&test.data),
                );
                assert_eq!(
                    codegen_message, runtime_message,
                    "codegen validate() message mismatch:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case, test.description,
                    pretty_json(&test.schema), pretty_json(&test.data),
                );

                // Compare codegen iter_errors() against runtime iter_errors()
                let runtime_iter: Vec<(String, String, String)> = validator
                    .iter_errors(&test.data)
                    .map(|e| {
                        (
                            e.to_string(),
                            e.schema_path().to_string(),
                            e.instance_path().to_string(),
                        )
                    })
                    .collect();
                let codegen_iter = codegen_validator.iter_errors(&test.data);
                assert_eq!(
                    codegen_iter,
                    runtime_iter,
                    "codegen iter_errors() mismatch:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );

                let evaluation = validator.evaluate(&test.data);
                assert!(
                    !evaluation.flag().valid,
                    "Evaluation output should be invalid:\nCase: {}\nTest: {}\nSchema: {}\nInstance: {}",
                    test.case,
                    test.description,
                    pretty_json(&test.schema),
                    pretty_json(&test.data),
                );
                let _ =
                    serde_json::to_value(evaluation.list()).expect("List output should serialize");
                let _ = serde_json::to_value(evaluation.hierarchical())
                    .expect("Hierarchical output should serialize");
            }
        }
    }

    fn pretty_json(v: &serde_json::Value) -> String {
        serde_json::to_string_pretty(v).expect("Failed to format JSON")
    }

    #[cfg(not(target_arch = "wasm32"))]
    #[test]
    fn test_instance_path() {
        let expectations: serde_json::Value =
            serde_json::from_str(include_str!("draft7_instance_paths.json")).expect("Valid JSON");
        for (filename, expected) in expectations.as_object().expect("Is object") {
            let test_file = fs::read_to_string(format!("tests/suite/tests/draft7/{filename}"))
                .unwrap_or_else(|_| panic!("Valid file: {filename}"));
            let data: serde_json::Value = serde_json::from_str(&test_file).expect("Valid JSON");
            for item in expected.as_array().expect("Is array") {
                let suite_id = usize::try_from(item["suite_id"].as_u64().expect("Is integer"))
                    .expect("suite_id fits in usize");
                let schema = &data[suite_id]["schema"];
                let validator = jsonschema::options()
                    .with_draft(Draft::Draft7)
                    .with_retriever(testsuite_retriever())
                    .build(schema)
                    .unwrap_or_else(|_| {
                        panic!(
                    "Valid schema. File: {filename}; Suite ID: {suite_id}; Schema: {schema}",
                )
                    });
                for test_data in item["tests"].as_array().expect("Valid array") {
                    let test_id = usize::try_from(test_data["id"].as_u64().expect("Is integer"))
                        .expect("test_id fits in usize");
                    let mut expected_instance_path = String::new();

                    for segment in test_data["instance_path"].as_array().expect("Valid array") {
                        expected_instance_path.push('/');
                        expected_instance_path.push_str(segment.as_str().expect("A string"));
                    }
                    let instance = &data[suite_id]["tests"][test_id]["data"];
                    let errors: Vec<_> = validator.iter_errors(instance).collect();
                    assert!(
                        !errors.is_empty(),
                        "\nFile: {}\nSuite: {}\nTest: {}",
                        filename,
                        data[suite_id]["description"],
                        data[suite_id]["tests"][test_id]["description"],
                    );

                    let mut found_paths = Vec::with_capacity(errors.len());
                    let mut matched = false;
                    for error in errors {
                        let actual_path = error.instance_path().as_str().to_string();
                        found_paths.push(actual_path.clone());
                        if actual_path == expected_instance_path {
                            matched = true;
                        }
                    }
                    assert!(
                        matched,
                        "\nFile: {}\nSuite: {}\nTest: {}\nExpected path: {}\nFound paths: {:?}",
                        filename,
                        data[suite_id]["description"],
                        data[suite_id]["tests"][test_id]["description"],
                        expected_instance_path,
                        found_paths
                    );
                }
            }
        }
    }

    fn should_skip_draft(draft: &str) -> bool {
        if let Some(filter) = allowed_draft_filter() {
            for entry in filter.split(',') {
                if entry.trim().eq_ignore_ascii_case(draft) {
                    return false;
                }
            }
            true
        } else {
            false
        }
    }

    fn allowed_draft_filter() -> Option<String> {
        #[cfg(not(target_arch = "wasm32"))]
        if let Ok(value) = env::var("JSONSCHEMA_SUITE_DRAFT_FILTER") {
            return Some(value);
        }
        option_env!("JSONSCHEMA_SUITE_DRAFT_FILTER").map(str::to_string)
    }
}
