use std::collections::{BTreeMap, BTreeSet};

use indexmap::IndexSet;
use oas3::spec::{ObjectOrReference, ObjectSchema, Schema, SchemaType, SchemaTypeSet};
use serde_json::{Value, json};

use crate::{
  generator::{
    ast::{EnumVariantToken, TypeRef, VariantContent, VariantDef},
    naming::{
      constants::{KNOWN_ENUM_VARIANT, REQUEST_BODY_SUFFIX},
      inference::{NormalizedVariant, derive_method_names, strip_common_affixes},
      name_index::{TypeNameIndex, compute_best_name, is_valid_common_name, longest_common_suffix},
    },
  },
  tests::common::create_test_spec,
  utils::{SchemaExt, SchemaMap},
};

fn make_string_schema() -> ObjectSchema {
  ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    ..Default::default()
  }
}

fn make_string_enum_schema(values: Vec<Value>) -> ObjectSchema {
  ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: values,
    ..Default::default()
  }
}

fn make_const_schema(value: Value) -> ObjectSchema {
  ObjectSchema {
    const_value: Some(value),
    ..Default::default()
  }
}

fn schema_object(schema: ObjectSchema) -> Schema {
  Schema::Object(Box::new(ObjectOrReference::Object(schema)))
}

fn schema_ref(name: &str) -> Schema {
  Schema::Object(Box::new(ObjectOrReference::Ref {
    ref_path: format!("#/components/schemas/{name}"),
    summary: None,
    description: None,
  }))
}

#[test]
fn test_derive_method_names() {
  let cases = vec![
    ("RequestType", vec!["GetRequest", "PostRequest"], vec!["get", "post"]),
    (
      "Status",
      vec!["StatusActive", "Active", "StatusInactive"],
      vec!["status_active", "active", "inactive"],
    ),
    (
      "ResponseFormatOption",
      vec!["ResponseFormatText", "ResponseFormatJson", "ResponseFormatXml"],
      vec!["text", "json", "xml"],
    ),
    ("Status", vec![], vec![]),
  ];

  for (enum_name, variants, expected) in cases {
    let variants_vec = variants.into_iter().map(String::from).collect::<Vec<String>>();
    assert_eq!(
      derive_method_names(enum_name, &variants_vec),
      expected,
      "Failed for enum '{enum_name}'"
    );
  }
}

#[test]
fn test_longest_common_suffix() {
  let cases = vec![
    (vec!["CreateUser", "UpdateUser"], "ateUser"),
    (vec!["Status", "Status"], "Status"),
    (vec!["Short", "Long"], ""),
    (vec!["ABStatus", "CDStatus"], "Status"),
    (vec!["PrefixOnly", "PrefixOther"], ""),
    (vec![], ""),
  ];

  for (inputs, expected) in cases {
    let input_strings = inputs.into_iter().map(String::from).collect::<Vec<String>>();
    let refs: Vec<&String> = input_strings.iter().collect();
    assert_eq!(
      longest_common_suffix(&refs),
      expected,
      "Failed for inputs {input_strings:?}"
    );
  }
}

#[test]
fn test_is_valid_common_name() {
  let cases = vec![
    ("User", true),
    ("UserResponse", true),
    ("Enum", false),
    ("Struct", false),
    ("Type", false),
    ("Typ", false),
    ("abc", false),
    ("123Type", false),
  ];

  for (input, expected) in cases {
    assert_eq!(is_valid_common_name(input), expected, "Failed for input '{input}'");
  }
}

#[test]
#[allow(clippy::type_complexity)]
fn test_compute_best_name() {
  let cases: Vec<(Vec<(&str, bool)>, Vec<&str>, &str)> = vec![
    (vec![("SingleCandidate", false)], vec![], "SingleCandidate"),
    (vec![("Collision", false)], vec!["Collision"], "Collision2"),
    (
      vec![("NewName", false), ("ExistingName", true)],
      vec!["ExistingName"],
      "ExistingName",
    ),
    (vec![("NetUser", false), ("WebUser", false)], vec![], "User"),
    (vec![("Cat", false), ("Bat", false)], vec![], "Cat"),
    (vec![("MyGroup", false), ("YourGroup", false)], vec!["Group"], "Group2"),
  ];

  for (candidate_pairs, used_list, expected) in cases {
    let candidates = candidate_pairs
      .iter()
      .map(|(s, b)| ((*s).to_string(), *b))
      .collect::<IndexSet<(String, bool)>>();
    let used = used_list.into_iter().map(String::from).collect::<BTreeSet<String>>();
    assert_eq!(
      compute_best_name(&candidates, &used),
      expected,
      "Failed for candidates {candidates:?} with used {used:?}"
    );
  }
}

#[test]
fn test_inline_type_scanner_known_suffix_patterns() {
  let voice_ids_shared = ObjectSchema {
    any_of: vec![
      schema_object(make_string_schema()),
      schema_object(make_string_enum_schema(vec![
        json!("alloy"),
        json!("ash"),
        json!("ballad"),
      ])),
    ],
    ..Default::default()
  };

  let format_type = ObjectSchema {
    any_of: vec![
      schema_object(make_string_schema()),
      schema_object(make_const_schema(json!("json"))),
      schema_object(make_const_schema(json!("text"))),
      schema_object(make_const_schema(json!("xml"))),
    ],
    ..Default::default()
  };

  let schemas = SchemaMap::from([
    ("VoiceIdsShared".to_string(), voice_ids_shared),
    ("FormatType".to_string(), format_type),
  ]);

  let spec = create_test_spec(BTreeMap::new());
  let index = TypeNameIndex::new(&schemas, &spec);
  let result = index.scan_and_compute_names().expect("Should scan successfully");

  let cases = [
    (
      vec!["alloy".to_string(), "ash".to_string(), "ballad".to_string()],
      "VoiceIdsSharedKnown",
      "StringEnumOptimizer pattern with enum_values",
    ),
    (
      vec!["json".to_string(), "text".to_string(), "xml".to_string()],
      "FormatTypeKnown",
      "anyOf with const values",
    ),
  ];

  for (enum_values, expected_name, description) in cases {
    let name = result.enum_names.get(&enum_values);
    assert_eq!(
      name,
      Some(&expected_name.to_string()),
      "{description} should be named with 'Known' suffix"
    );
  }
}

#[test]
fn test_inline_type_scanner_enum_naming_without_known_suffix() {
  let status = make_string_enum_schema(vec![json!("active"), json!("inactive"), json!("pending")]);

  let chat_model = make_string_enum_schema(vec![json!("gpt-4"), json!("gpt-3.5-turbo")]);

  let model_ids_shared = ObjectSchema {
    any_of: vec![schema_object(make_string_schema()), schema_ref("ChatModel")],
    ..Default::default()
  };

  let schemas = SchemaMap::from([
    ("Status".to_string(), status),
    ("ChatModel".to_string(), chat_model),
    ("ModelIdsShared".to_string(), model_ids_shared),
  ]);

  let spec = create_test_spec(BTreeMap::new());
  let index = TypeNameIndex::new(&schemas, &spec);
  let result = index.scan_and_compute_names().expect("Should scan successfully");

  let mut status_values = vec!["active".to_string(), "inactive".to_string(), "pending".to_string()];
  status_values.sort();
  let status_name = result.enum_names.get(&status_values);
  assert!(status_name.is_some(), "Regular enum should have a precomputed name");
  assert!(
    !status_name.unwrap().ends_with(KNOWN_ENUM_VARIANT),
    "Regular enum should not have 'Known' suffix"
  );

  let mut chat_model_values = vec!["gpt-4".to_string(), "gpt-3.5-turbo".to_string()];
  chat_model_values.sort();
  let chat_model_name = result.enum_names.get(&chat_model_values);
  assert!(
    chat_model_name.is_some(),
    "Top-level enum referenced by anyOf should have a precomputed name"
  );
  assert!(
    chat_model_name.unwrap().starts_with("ChatModel"),
    "Precomputed name should be based on ChatModel"
  );
}

#[test]
fn test_infer_name_from_context() {
  let cases = [
    (
      ObjectSchema::default(),
      "/api/check-access-by-email",
      "200",
      "check_access_by_email200Response",
      "should sanitize hyphens",
    ),
    (
      ObjectSchema::default(),
      "/api/foo-bar.baz_qux",
      "201",
      "foo_bar_baz_qux201Response",
      "should sanitize multiple separators",
    ),
    (
      ObjectSchema::default(),
      "/api/create-user",
      REQUEST_BODY_SUFFIX,
      "create_userRequestBody",
      "should handle request body suffix",
    ),
  ];

  for (schema, path, status, expected, description) in cases {
    let result = schema.infer_name_from_context(path, status);
    assert_eq!(result, expected, "Failed: {description}");
    assert!(
      !result.contains('-') && !result.contains('.'),
      "Result should not contain hyphens or dots: {result}"
    );
  }

  let mut schema_with_property = ObjectSchema::default();
  schema_with_property
    .properties
    .insert("user".to_string(), schema_object(ObjectSchema::default()));
  let result = schema_with_property.infer_name_from_context("/api/check-access", "200");
  assert_eq!(
    result, "userResponse",
    "Single property response should use property name"
  );
}

#[test]
fn test_normalize_strings() {
  let cases = [
    (json!("active"), "Active", "active"),
    (json!("pending_approval"), "PendingApproval", "pending_approval"),
    (json!("pending-approval"), "PendingApproval", "pending-approval"),
  ];

  for (val, expected_name, expected_rename) in cases {
    let res = NormalizedVariant::try_from(&val).unwrap();
    assert_eq!(res.name, expected_name, "name mismatch for {val:?}");
    assert_eq!(res.rename_value, expected_rename, "rename mismatch for {val:?}");
  }
}

#[test]
#[allow(clippy::approx_constant)]
fn test_normalize_numbers() {
  let cases = [
    (json!(404), "Value404", "404"),
    (json!(-42), "Value_42", "-42"),
    (json!(0), "Value0", "0"),
    (json!(3.14), "Value3_14", "3.14"),
    (json!(-2.5), "Value_2_5", "-2.5"),
  ];

  for (val, expected_name, expected_rename) in cases {
    let res = NormalizedVariant::try_from(&val).unwrap();
    assert_eq!(res.name, expected_name, "name mismatch for {val:?}");
    assert_eq!(res.rename_value, expected_rename, "rename mismatch for {val:?}");
  }
}

#[test]
fn test_normalize_booleans_and_unsupported_types() {
  let true_res = NormalizedVariant::try_from(&json!(true)).unwrap();
  assert_eq!(true_res.name, "True");
  assert_eq!(true_res.rename_value, "true");

  let false_res = NormalizedVariant::try_from(&json!(false)).unwrap();
  assert_eq!(false_res.name, "False");
  assert_eq!(false_res.rename_value, "false");

  assert!(NormalizedVariant::try_from(&json!(null)).is_err());
  assert!(NormalizedVariant::try_from(&json!({"key": "value"})).is_err());
  assert!(NormalizedVariant::try_from(&json!([1, 2, 3])).is_err());
}

#[test]
fn test_infer_variant_name_single_types() {
  let type_cases = [
    (SchemaType::String, "String"),
    (SchemaType::Number, "Number"),
    (SchemaType::Integer, "Integer"),
    (SchemaType::Boolean, "Boolean"),
    (SchemaType::Array, "Array"),
    (SchemaType::Object, "Object"),
    (SchemaType::Null, "Null"),
  ];

  for (schema_type, expected) in type_cases {
    let schema = ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(schema_type)),
      ..Default::default()
    };
    assert_eq!(
      schema.infer_variant_name(0),
      expected,
      "type mismatch for {schema_type:?}"
    );
  }
}

#[test]
fn test_infer_variant_name_special_cases() {
  let enum_schema = ObjectSchema {
    enum_values: vec![json!("a"), json!("b")],
    ..Default::default()
  };
  assert_eq!(enum_schema.infer_variant_name(0), "Enum");

  let multi_type_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Multiple(vec![SchemaType::String, SchemaType::Number])),
    ..Default::default()
  };
  assert_eq!(multi_type_schema.infer_variant_name(0), "Mixed");

  let no_type_schema = ObjectSchema::default();
  assert_eq!(no_type_schema.infer_variant_name(5), "Variant5");
}

#[test]
fn test_infer_variant_name_object_variants() {
  let bare_object = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  assert_eq!(
    bare_object.infer_variant_name(0),
    "Object",
    "bare object without properties should be Object"
  );

  let object_with_single_property = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [("items".to_string(), schema_object(ObjectSchema::default()))]
      .into_iter()
      .collect(),
    ..Default::default()
  };
  assert_eq!(
    object_with_single_property.infer_variant_name(0),
    "Items",
    "object with single property should use property name"
  );

  let object_with_single_required = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [
      ("id".to_string(), schema_object(ObjectSchema::default())),
      ("name".to_string(), schema_object(ObjectSchema::default())),
    ]
    .into_iter()
    .collect(),
    required: vec!["id".to_string()],
    ..Default::default()
  };
  assert_eq!(
    object_with_single_required.infer_variant_name(0),
    "Id",
    "object with single required field should use required field name"
  );

  let object_with_multiple_properties = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [
      ("id".to_string(), schema_object(ObjectSchema::default())),
      ("name".to_string(), schema_object(ObjectSchema::default())),
    ]
    .into_iter()
    .collect(),
    ..Default::default()
  };
  assert_eq!(
    object_with_multiple_properties.infer_variant_name(0),
    "Object",
    "object with multiple properties and no single required should be Object"
  );
}

fn make_variant(name: &str) -> VariantDef {
  VariantDef::builder()
    .name(EnumVariantToken::from(name))
    .content(VariantContent::Unit)
    .build()
}

#[test]
fn test_strip_common_affixes_no_op_cases() {
  let empty: Vec<VariantDef> = vec![];
  let empty = strip_common_affixes(empty);
  assert!(empty.is_empty());

  let single = vec![make_variant("UserResponse")];
  let single = strip_common_affixes(single);
  assert_eq!(single[0].name, EnumVariantToken::new("UserResponse"));
}

#[test]
fn test_strip_common_affixes_strips_prefix_suffix_or_both() {
  let suffix_variants = vec![make_variant("CreateResponse"), make_variant("UpdateResponse")];
  let suffix_variants = strip_common_affixes(suffix_variants);
  assert_eq!(suffix_variants[0].name, EnumVariantToken::new("Create"));
  assert_eq!(suffix_variants[1].name, EnumVariantToken::new("Update"));

  let prefix_variants = vec![make_variant("UserCreate"), make_variant("UserUpdate")];
  let prefix_variants = strip_common_affixes(prefix_variants);
  assert_eq!(prefix_variants[0].name, EnumVariantToken::new("Create"));
  assert_eq!(prefix_variants[1].name, EnumVariantToken::new("Update"));

  let both_variants = vec![make_variant("UserCreateResponse"), make_variant("UserUpdateResponse")];
  let both_variants = strip_common_affixes(both_variants);
  assert_eq!(both_variants[0].name, EnumVariantToken::new("Create"));
  assert_eq!(both_variants[1].name, EnumVariantToken::new("Update"));
}

#[test]
fn test_strip_common_affixes_no_common_parts() {
  let variants = vec![make_variant("CreateUser"), make_variant("DeletePost")];
  let variants = strip_common_affixes(variants);
  assert_eq!(variants[0].name, EnumVariantToken::new("CreateUser"));
  assert_eq!(variants[1].name, EnumVariantToken::new("DeletePost"));
}

#[test]
fn test_strip_common_affixes_safety_guards() {
  let collision_variants = vec![make_variant("UserResponse"), make_variant("UserResponse")];
  let collision_variants = strip_common_affixes(collision_variants);
  assert_eq!(collision_variants[0].name, EnumVariantToken::new("UserResponse"));
  assert_eq!(collision_variants[1].name, EnumVariantToken::new("UserResponse"));

  let empty_name_variants = vec![make_variant("Response"), make_variant("Response")];
  let empty_name_variants = strip_common_affixes(empty_name_variants);
  assert_eq!(empty_name_variants[0].name, EnumVariantToken::new("Response"));
  assert_eq!(empty_name_variants[1].name, EnumVariantToken::new("Response"));
}

#[test]
fn test_strip_common_affixes_preserves_variant_content() {
  let tuple_type = TypeRef::new("TestStruct");
  let variants = vec![
    VariantDef::builder()
      .name(EnumVariantToken::from("CreateResponse"))
      .content(VariantContent::Tuple(vec![tuple_type]))
      .build(),
    make_variant("UpdateResponse"),
  ];

  let variants = strip_common_affixes(variants);

  assert_eq!(variants[0].name, EnumVariantToken::new("Create"));
  assert_eq!(variants[1].name, EnumVariantToken::new("Update"));
  assert!(matches!(variants[0].content, VariantContent::Tuple(_)));
  assert!(matches!(variants[1].content, VariantContent::Unit));
}

#[test]
fn schema_ext_is_primitive() {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    ..ObjectSchema::default()
  };
  assert!(schema.is_primitive(), "string without properties should be primitive");

  schema
    .properties
    .insert("foo".to_string(), schema_object(ObjectSchema::default()));
  assert!(!schema.is_primitive(), "schema with properties should not be primitive");

  let string_enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("auto"), json!("none")],
    ..ObjectSchema::default()
  };
  assert!(
    !string_enum_schema.is_primitive(),
    "string enum with 2+ values should NOT be primitive"
  );

  let single_enum_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
    enum_values: vec![json!("only_value")],
    ..ObjectSchema::default()
  };
  assert!(
    single_enum_schema.is_primitive(),
    "string enum with 1 value should be primitive (const-like)"
  );
}

#[test]
fn schema_ext_is_nullable_object() {
  let null_only = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Null)),
    ..ObjectSchema::default()
  };
  assert!(null_only.is_nullable_object(), "null-only schema should be nullable");

  let nullable_object = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Multiple(vec![SchemaType::Object, SchemaType::Null])),
    ..ObjectSchema::default()
  };
  assert!(
    nullable_object.is_nullable_object(),
    "object|null without properties should be nullable"
  );

  let mut nullable_with_props = nullable_object.clone();
  nullable_with_props
    .properties
    .insert("x".into(), schema_object(ObjectSchema::default()));
  assert!(
    !nullable_with_props.is_nullable_object(),
    "object|null with properties should not be nullable"
  );
}

#[test]
fn schema_ext_has_inline_union_array_items() {
  use crate::tests::common::create_test_graph;

  let type_a = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [("field_a".to_string(), schema_object(make_string_schema()))]
      .into_iter()
      .collect(),
    ..Default::default()
  };
  let type_b = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [("field_b".to_string(), schema_object(make_string_schema()))]
      .into_iter()
      .collect(),
    ..Default::default()
  };

  let graph = create_test_graph(BTreeMap::from([
    ("TypeA".to_string(), type_a),
    ("TypeB".to_string(), type_b),
  ]));

  let array_with_union_items = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
    items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Object(
      ObjectSchema {
        one_of: vec![schema_ref("TypeA"), schema_ref("TypeB")],
        ..Default::default()
      },
    ))))),
    ..Default::default()
  };

  assert!(
    array_with_union_items.has_inline_union_array_items(graph.spec()),
    "array with oneOf items should have union items"
  );

  let array_with_string_items = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
    items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Object(
      make_string_schema(),
    ))))),
    ..Default::default()
  };

  assert!(
    !array_with_string_items.has_inline_union_array_items(graph.spec()),
    "array with string items should not have union items"
  );

  let array_with_ref_items = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
    items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Ref {
      ref_path: "#/components/schemas/TypeA".to_string(),
      summary: None,
      description: None,
    })))),
    ..Default::default()
  };

  assert!(
    !array_with_ref_items.has_inline_union_array_items(graph.spec()),
    "array with ref items should not have inline union items"
  );

  let array_without_items = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
    items: None,
    ..Default::default()
  };

  assert!(
    !array_without_items.has_inline_union_array_items(graph.spec()),
    "array without items should not have union items"
  );
}

#[test]
fn extract_common_variant_prefix_cases() {
  use crate::generator::naming::inference::extract_common_variant_prefix;

  struct Case {
    variants: Vec<&'static str>,
    expected: Option<&'static str>,
    description: &'static str,
  }

  let cases = [
    Case {
      variants: vec![
        "BetaResponseCharLocationCitation",
        "BetaResponseUrlCitation",
        "BetaResponseFileCitation",
      ],
      expected: Some("BetaCitation"),
      description: "first prefix (Beta) + suffix (Citation) - terse naming",
    },
    Case {
      variants: vec!["ContentBlockStart", "ContentBlockDelta", "ContentBlockStop"],
      expected: Some("ContentBlock"),
      description: "full common prefix (ContentBlock), no suffix",
    },
    Case {
      variants: vec!["Tool", "BashTool"],
      expected: None,
      description: "no common prefix (Tool vs Bash) - returns None",
    },
    Case {
      variants: vec!["TypeA", "TypeB", "TypeC"],
      expected: Some("Type"),
      description: "common prefix (Type) only, no suffix",
    },
    Case {
      variants: vec!["AlphaFoo", "BetaBar", "GammaQux"],
      expected: None,
      description: "no common prefix - returns None",
    },
    Case {
      variants: vec!["RequestBody"],
      expected: None,
      description: "single variant - returns None",
    },
    Case {
      variants: vec![],
      expected: None,
      description: "empty variants - returns None",
    },
    Case {
      variants: vec!["ApiErrorNotFound", "ApiErrorBadRequest", "ApiErrorUnauthorized"],
      expected: Some("ApiError"),
      description: "full common prefix (ApiError), no suffix",
    },
    Case {
      variants: vec!["BetaMessageStartEvent", "BetaMessageDeltaEvent", "BetaMessageStopEvent"],
      expected: Some("BetaEvent"),
      description: "first prefix (Beta) + suffix (Event) - terse naming",
    },
    Case {
      variants: vec!["FooBarBaz", "FooQuxBaz"],
      expected: Some("FooBaz"),
      description: "first prefix (Foo) + suffix (Baz) - terse naming",
    },
    Case {
      variants: vec!["StreamEventStart", "StreamEventData", "StreamEventEnd"],
      expected: Some("StreamEvent"),
      description: "full common prefix (StreamEvent), no suffix",
    },
  ];

  for case in cases {
    let variants = case
      .variants
      .iter()
      .map(|name| ObjectOrReference::Ref {
        ref_path: format!("#/components/schemas/{name}"),
        summary: None,
        description: None,
      })
      .collect::<Vec<ObjectOrReference<ObjectSchema>>>();

    let result = extract_common_variant_prefix(&variants);

    assert_eq!(
      result.as_ref().map(|r| r.name.as_str()),
      case.expected,
      "{}: expected {:?}, got {:?}",
      case.description,
      case.expected,
      result.as_ref().map(|r| r.name.as_str())
    );
  }
}

#[test]
fn test_union_schemas_do_not_generate_enum_candidates() {
  let integer_or_inf_union = ObjectSchema {
    any_of: vec![
      schema_object(ObjectSchema {
        schema_type: Some(SchemaTypeSet::Single(SchemaType::Integer)),
        ..Default::default()
      }),
      schema_object(make_const_schema(json!("inf"))),
    ],
    ..Default::default()
  };

  let parent_schema_a = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [(
      "max_output_tokens".to_string(),
      schema_object(integer_or_inf_union.clone()),
    )]
    .into_iter()
    .collect(),
    ..Default::default()
  };

  let parent_schema_b = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [("max_output_tokens".to_string(), schema_object(integer_or_inf_union))]
      .into_iter()
      .collect(),
    ..Default::default()
  };

  let schemas = SchemaMap::from([
    ("ParentA".to_string(), parent_schema_a),
    ("ParentB".to_string(), parent_schema_b),
  ]);

  let spec = create_test_spec(BTreeMap::new());
  let index = TypeNameIndex::new(&schemas, &spec);
  let result = index.scan_and_compute_names().expect("Should scan successfully");

  let inf_values = vec!["inf".to_string()];
  assert!(
    !result.enum_names.contains_key(&inf_values),
    "Union schemas with const values should NOT generate enum candidates. \
     This prevents name collision between enum and schema precomputed names."
  );

  assert!(
    !result.names.is_empty(),
    "Schema candidates should still be collected for the union types"
  );
}

#[test]
fn test_regular_enums_still_generate_enum_candidates() {
  let status_enum = make_string_enum_schema(vec![json!("active"), json!("pending"), json!("done")]);

  let parent_schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    properties: [("status".to_string(), schema_object(status_enum))]
      .into_iter()
      .collect(),
    ..Default::default()
  };

  let schemas = SchemaMap::from([("Parent".to_string(), parent_schema)]);

  let spec = create_test_spec(BTreeMap::new());
  let index = TypeNameIndex::new(&schemas, &spec);
  let result = index.scan_and_compute_names().expect("Should scan successfully");

  let mut status_values = vec!["active".to_string(), "pending".to_string(), "done".to_string()];
  status_values.sort();
  assert!(
    result.enum_names.contains_key(&status_values),
    "Regular string enums (not unions) should still generate enum candidates"
  );
}
