use std::{collections::BTreeSet, sync::Arc};

use serde_json::json;

use crate::{
  generator::{
    ast::{
      DeriveTrait, DerivesProvider, EnumDef, EnumMethodKind, EnumToken, EnumVariantToken, MethodNameToken,
      RustPrimitive, RustType, SerdeAttribute, TypeRef, VariantContent, VariantDef,
    },
    converter::{
      SchemaConverter,
      union_types::CollisionStrategy,
      unions::{EnumConverter, UnionConverter},
    },
    metrics::GenerationStats,
    naming::constants::KNOWN_ENUM_VARIANT,
    schema_registry::SchemaRegistry,
  },
  tests::common::{
    config_with_no_helpers, config_with_preserve_case, config_with_sorted_enums, create_test_context,
    create_test_graph, default_config, parse_schema, parse_schemas,
  },
};

#[test]
fn test_simple_string_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "SimpleEnum",
    json!({
      "type": "string",
      "enum": ["value1", "value2"]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("SimpleEnum", graph.get("SimpleEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "SimpleEnum");
  assert_eq!(enum_def.variants.len(), 2);
  assert!(enum_def.derives().contains(&DeriveTrait::Eq));
  assert!(enum_def.derives().contains(&DeriveTrait::Hash));
  Ok(())
}

#[test]
fn test_oneof_with_discriminator_has_rename_attrs() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "oneOf": [
          { "$ref": "#/components/schemas/VariantA" },
          { "$ref": "#/components/schemas/VariantB" }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "type_a": "#/components/schemas/VariantA",
            "type_b": "#/components/schemas/VariantB"
          }
        }
      }),
    ),
    (
      "VariantA",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "type_a" }
        }
      }),
    ),
    (
      "VariantB",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "type_b" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::DiscriminatedEnum(enum_def) = result.last().unwrap() else {
    panic!("Expected DiscriminatedEnum as last type")
  };

  assert_eq!(enum_def.name.to_string(), "TestUnion");
  assert_eq!(enum_def.discriminator_field, "type");
  assert_eq!(enum_def.variants.len(), 2);
  let variant_values = enum_def
    .variants
    .iter()
    .flat_map(|v| v.discriminator_values.iter().map(String::as_str))
    .collect::<BTreeSet<_>>();
  assert!(variant_values.contains("type_a"));
  assert!(variant_values.contains("type_b"));
  Ok(())
}

#[test]
fn test_anyof_without_discriminator_has_no_rename_attrs() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "anyOf": [
          { "$ref": "#/components/schemas/VariantA" },
          { "$ref": "#/components/schemas/VariantB" }
        ]
      }),
    ),
    (
      "VariantA",
      json!({
        "type": "object",
        "properties": {
          "field1": { "type": "string" }
        }
      }),
    ),
    (
      "VariantB",
      json!({
        "type": "object",
        "properties": {
          "field2": { "type": "integer" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum as last type")
  };

  assert_eq!(enum_def.name.to_string(), "TestUnion");
  assert_eq!(enum_def.variants.len(), 2);
  assert!(enum_def.variants[0].serde_attrs.is_empty());
  assert!(enum_def.variants[1].serde_attrs.is_empty());
  assert!(enum_def.serde_attrs.contains(&SerdeAttribute::Untagged));
  Ok(())
}

#[test]
fn test_anyof_with_discriminator_no_untagged() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "anyOf": [
          { "$ref": "#/components/schemas/VariantA" },
          { "$ref": "#/components/schemas/VariantB" }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "type_a": "#/components/schemas/VariantA",
            "type_b": "#/components/schemas/VariantB"
          }
        }
      }),
    ),
    (
      "VariantA",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "type_a" }
        }
      }),
    ),
    (
      "VariantB",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "type_b" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::DiscriminatedEnum(enum_def) = result.last().unwrap() else {
    panic!("Expected DiscriminatedEnum as last type")
  };

  assert_eq!(enum_def.name.to_string(), "TestUnion");
  assert_eq!(enum_def.discriminator_field, "type");
  assert_eq!(enum_def.variants.len(), 2);
  Ok(())
}

#[test]
fn test_empty_enum_converts_to_string() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "EmptyEnum",
    json!({
      "type": "string",
      "enum": []
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("EmptyEnum", graph.get("EmptyEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::TypeAlias(alias) = &result[0] else {
    panic!("Expected type alias for empty enum")
  };

  assert_eq!(alias.name, "EmptyEnum");
  assert_eq!(alias.target.to_rust_type(), "String");
  Ok(())
}

#[test]
#[allow(clippy::approx_constant)]
fn test_float_enum_values() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "FloatEnum",
    json!({
      "type": "number",
      "enum": [0.0, 1.5, 3.14, -2.5]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("FloatEnum", graph.get("FloatEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "FloatEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("Value0"));
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("0".to_string()))
  );
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Value1_5"));
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("1.5".to_string()))
  );
  Ok(())
}

#[test]
fn test_boolean_enum_values() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "BoolEnum",
    json!({
      "type": "boolean",
      "enum": [true, false]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("BoolEnum", graph.get("BoolEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "BoolEnum");
  assert_eq!(enum_def.variants.len(), 2);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("True"));
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("true".to_string()))
  );
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("False"));
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("false".to_string()))
  );
  Ok(())
}

#[test]
fn test_mixed_type_enum_values() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "MixedEnum",
    json!({
      "enum": ["string", 42, 1.5, true]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("MixedEnum", graph.get("MixedEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "MixedEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("String"));
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Value42"));
  assert_eq!(enum_def.variants[2].name, EnumVariantToken::new("Value1_5"));
  assert_eq!(enum_def.variants[3].name, EnumVariantToken::new("True"));
  Ok(())
}

#[test]
fn test_integer_enum_values() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "IntEnum",
    json!({
      "type": "integer",
      "enum": [0, 1, 42, -5]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("IntEnum", graph.get("IntEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "IntEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("Value0"));
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("0".to_string()))
  );
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Value1"));
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("1".to_string()))
  );
  assert_eq!(enum_def.variants[2].name, EnumVariantToken::new("Value42"));
  assert!(
    enum_def.variants[2]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("42".to_string()))
  );
  assert_eq!(enum_def.variants[3].name, EnumVariantToken::new("Value_5"));
  assert!(
    enum_def.variants[3]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("-5".to_string()))
  );
  Ok(())
}

#[test]
fn test_case_insensitive_duplicates_with_preservation() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "CaseEnum",
    json!({
      "type": "string",
      "enum": ["ITEM", "item", "SELECT", "select"]
    }),
  )]));
  let context = create_test_context(graph.clone(), config_with_preserve_case());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("CaseEnum", graph.get("CaseEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "CaseEnum");
  assert_eq!(enum_def.variants.len(), 4);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("Item"));
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("ITEM".to_string()))
  );
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Item1"));
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("item".to_string()))
  );
  assert_eq!(enum_def.variants[2].name, EnumVariantToken::new("Select"));
  assert!(
    enum_def.variants[2]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("SELECT".to_string()))
  );
  assert_eq!(enum_def.variants[3].name, EnumVariantToken::new("Select3"));
  assert!(
    enum_def.variants[3]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("select".to_string()))
  );
  Ok(())
}

#[test]
fn test_case_insensitive_duplicates_with_deduplication() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "CaseEnum",
    json!({
      "type": "string",
      "enum": ["ITEM", "item", "SELECT", "select"]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("CaseEnum", graph.get("CaseEnum").unwrap())?;

  assert_eq!(result.len(), 1);
  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.name.to_string(), "CaseEnum");
  assert_eq!(enum_def.variants.len(), 2);
  assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("Item"));
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("ITEM".to_string()))
  );
  assert!(
    enum_def.variants[0]
      .serde_attrs
      .contains(&SerdeAttribute::Alias("item".to_string()))
  );
  assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Select"));
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Rename("SELECT".to_string()))
  );
  assert!(
    enum_def.variants[1]
      .serde_attrs
      .contains(&SerdeAttribute::Alias("select".to_string()))
  );
  Ok(())
}

#[test]
fn test_collision_strategy_enum() {
  let s1 = CollisionStrategy::Preserve;
  let s2 = CollisionStrategy::Deduplicate;
  assert_ne!(s1, s2);
}

#[test]
fn test_preserve_strategy_with_multiple_collisions() {
  let graph = create_test_graph(parse_schemas(vec![]));
  let context = create_test_context(graph, config_with_preserve_case());
  let converter = EnumConverter::new(context);

  let schema = parse_schema(json!({
    "enum": ["active", "Active", "ACTIVE"]
  }));

  let result = converter.convert_value_enum("Status", &schema);

  if let RustType::Enum(enum_def) = result {
    assert_eq!(enum_def.variants.len(), 3);
    assert_eq!(enum_def.variants[0].name, EnumVariantToken::new("Active"));
    assert_eq!(enum_def.variants[1].name, EnumVariantToken::new("Active1"));
    assert_eq!(enum_def.variants[2].name, EnumVariantToken::new("Active2"));
  } else {
    panic!("Expected enum result");
  }
}

#[test]
fn test_relaxed_enum_detects_freeform_pattern() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "anyOf": [
      { "type": "string", "const": "known1" },
      { "type": "string", "const": "known2" },
      { "type": "string" }
    ]
  }));

  let result = union_converter.convert_union("TestEnum", &schema);
  assert!(result.is_ok());

  let output = result.unwrap();
  let types = output.into_vec();
  assert_eq!(types.len(), 2);

  let has_known_enum = types.iter().any(|t| match t {
    RustType::Enum(e) => e.name == EnumToken::new("TestEnumKnown"),
    _ => false,
  });
  let outer_enum = types.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == EnumToken::new("TestEnum") => Some(e),
    _ => None,
  });

  assert!(has_known_enum);
  assert!(outer_enum.is_some(), "should have outer wrapper enum");

  let outer_enum = outer_enum.unwrap();
  assert_eq!(outer_enum.methods.len(), 2, "wrapper enum should have 2 helper methods");
  assert!(
    outer_enum.methods.iter().any(|m| m.name.as_str() == "known1"),
    "should have known1 method"
  );
  assert!(
    outer_enum.methods.iter().any(|m| m.name.as_str() == "known2"),
    "should have known2 method"
  );
}

#[test]
fn test_relaxed_enum_rejects_no_freeform() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "anyOf": [
      { "type": "string", "const": "known1" },
      { "type": "string", "const": "known2" }
    ]
  }));

  let result = union_converter.convert_union("TestEnum", &schema);
  assert!(result.is_ok());
  let output = result.unwrap();
  let types = output.into_vec();
  assert!(
    !types
      .iter()
      .any(|t| matches!(t, RustType::Enum(e) if e.name == EnumToken::new("TestEnumKnown"))),
    "Should not generate relaxed enum without freeform string variant"
  );
}

#[test]
fn test_anyof_with_const_generates_unit_variant() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "ResponseFormat",
      json!({
        "description": "Response format option",
        "anyOf": [
          {
            "type": "string",
            "const": "auto",
            "description": "`auto` is the default value"
          },
          { "$ref": "#/components/schemas/TextFormat" }
        ]
      }),
    ),
    (
      "TextFormat",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "text" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("ResponseFormat", graph.get("ResponseFormat").unwrap())?;

  assert!(!result.is_empty());
  let RustType::Enum(enum_def) = &result[result.len() - 1] else {
    panic!("Expected enum as last type, got: {result:?}");
  };

  assert_eq!(enum_def.name.to_string(), "ResponseFormat");
  assert_eq!(enum_def.variants.len(), 2);

  let auto_variant = &enum_def.variants[0];
  assert_eq!(auto_variant.name, EnumVariantToken::new("Auto"));
  assert!(matches!(auto_variant.content, VariantContent::Unit));
  assert_eq!(
    auto_variant.serde_attrs,
    vec![SerdeAttribute::Rename("auto".to_string())]
  );

  let text_variant = &enum_def.variants[1];
  assert_eq!(text_variant.name, EnumVariantToken::new("TextFormat"));
  assert!(matches!(text_variant.content, VariantContent::Tuple(_)));

  Ok(())
}

#[test]
fn test_const_unit_variant_in_enum() {
  use serde::{Deserialize, Serialize};

  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
  #[serde(default)]
  struct DataVariant {
    #[serde(rename = "type")]
    r#type: String,
    value: i32,
  }

  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  #[serde(untagged)]
  enum TestEnum {
    #[serde(rename = "auto")]
    Auto,
    Data(DataVariant),
  }

  let auto = TestEnum::Auto;
  let data = TestEnum::Data(DataVariant {
    r#type: "data".to_string(),
    value: 42,
  });

  let auto_json = serde_json::to_string::<TestEnum>(&auto).unwrap();
  let data_json = serde_json::to_value::<&TestEnum>(&data).unwrap();

  assert_eq!(auto_json, "null");
  assert_eq!(data_json["type"], "data");
  assert_eq!(data_json["value"], 42);
}

#[test]
fn test_openapi_response_format_serialization() {
  use serde::{Deserialize, Serialize};

  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
  #[serde(default)]
  struct ResponseFormatText {
    #[serde(rename = "type")]
    #[serde(default = "default_text_type")]
    r#type: String,
  }

  fn default_text_type() -> String {
    "text".to_string()
  }

  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
  #[serde(default)]
  struct ResponseFormatJsonSchema {
    #[serde(rename = "type")]
    #[serde(default = "default_json_schema_type")]
    r#type: String,
    json_schema: serde_json::Value,
  }

  fn default_json_schema_type() -> String {
    "json_schema".to_string()
  }

  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  #[serde(untagged)]
  enum ResponseFormat {
    #[serde(rename = "auto")]
    Auto,
    Text(ResponseFormatText),
    JsonSchema(ResponseFormatJsonSchema),
  }

  let text = ResponseFormat::Text(ResponseFormatText {
    r#type: "text".to_string(),
  });
  let json_schema = ResponseFormat::JsonSchema(ResponseFormatJsonSchema {
    r#type: "json_schema".to_string(),
    json_schema: serde_json::json!({"type": "object"}),
  });

  let text_json = serde_json::to_value::<&ResponseFormat>(&text).unwrap();
  let json_schema_json = serde_json::to_value::<&ResponseFormat>(&json_schema).unwrap();

  assert_eq!(text_json["type"], "text");
  assert_eq!(json_schema_json["type"], "json_schema");
  assert_eq!(json_schema_json["json_schema"]["type"], "object");
}

#[test]
fn test_enum_helper_methods_generation() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "TestUnion",
    json!({
      "oneOf": [
        {
          "title": "Simple",
          "type": "object",
          "properties": {
            "opt_field": { "type": "string" }
          }
        },
        {
          "title": "SingleParam",
          "type": "object",
          "properties": {
            "req_field": { "type": "string" }
          },
          "required": ["req_field"]
        },
        {
          "title": "Complex",
          "type": "object",
          "properties": {
            "req1": { "type": "string" },
            "req2": { "type": "string" }
          },
          "required": ["req1", "req2"]
        }
      ]
    }),
  )]));

  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.methods.len(), 2);

  let simple_method = enum_def
    .methods
    .iter()
    .find(|m| m.name == "simple")
    .expect("simple method not found");
  match &simple_method.kind {
    EnumMethodKind::ParameterizedConstructor {
      variant_name,
      wrapped_type,
      param_name,
      param_type,
    } => {
      assert_eq!(variant_name, &EnumVariantToken::from("Simple"));
      assert_eq!(wrapped_type.to_rust_type(), "TestUnionSimple");
      assert_eq!(param_name, "opt_field");
      assert_eq!(param_type.to_rust_type(), "Option<String>");
    }
    _ => panic!("Expected ParameterizedConstructor for single optional field"),
  }

  let param_method = enum_def
    .methods
    .iter()
    .find(|m| m.name == "single_param")
    .expect("single_param method not found");
  match &param_method.kind {
    EnumMethodKind::ParameterizedConstructor {
      variant_name,
      wrapped_type,
      param_name,
      param_type,
    } => {
      assert_eq!(variant_name, &EnumVariantToken::from("SingleParam"));
      assert_eq!(wrapped_type.to_rust_type(), "TestUnionSingleParam");
      assert_eq!(param_name, "req_field");
      assert_eq!(param_type.to_rust_type(), "String");
    }
    _ => panic!("Expected ParameterizedConstructor"),
  }

  Ok(())
}

#[test]
fn test_enum_helper_methods_disabled_flag() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "TestUnion",
    json!({
      "oneOf": [
        {
          "title": "Simple",
          "type": "object",
          "properties": {
            "opt_field": { "type": "string" }
          }
        }
      ]
    }),
  )]));

  let context = create_test_context(graph.clone(), config_with_no_helpers());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum")
  };

  assert!(enum_def.methods.is_empty());
  Ok(())
}

#[test]
fn test_enum_helper_naming_stripping() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "ResponseFormat",
    json!({
      "oneOf": [
        {
          "title": "ResponseFormatText",
          "type": "object",
          "properties": {
            "dummy": { "type": "string" }
          }
        }
      ]
    }),
  )]));

  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("ResponseFormat", graph.get("ResponseFormat").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum")
  };

  let method = enum_def.methods.first().unwrap();
  assert_eq!(method.name, "text");

  Ok(())
}

#[test]
fn test_enum_helper_method_name_collision() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "Status",
    json!({
      "oneOf": [
        {
          "title": "StatusActive",
          "type": "object",
          "properties": {
            "opt_field": { "type": "string" }
          }
        },
        {
          "title": "Active",
          "type": "object",
          "properties": {
            "opt_field2": { "type": "string" }
          }
        }
      ]
    }),
  )]));

  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Status", graph.get("Status").unwrap())?;

  let RustType::Enum(enum_def) = result.last().unwrap() else {
    panic!("Expected enum")
  };

  assert_eq!(enum_def.methods.len(), 2);
  let names = enum_def.methods.iter().map(|m| m.name.clone()).collect::<Vec<_>>();
  assert!(names.contains(&MethodNameToken::from("active")));
  assert!(
    names.contains(&MethodNameToken::from("active2")) || names.iter().any(|n| n != &MethodNameToken::from("active"))
  );

  Ok(())
}

#[test]
fn test_enum_helper_skips_without_default_trait() {
  let enum_def = RustType::Enum(EnumDef {
    name: EnumToken::new("TestEnum"),
    variants: vec![
      VariantDef::builder()
        .name(EnumVariantToken::new("Variant"))
        .content(VariantContent::Tuple(vec![TypeRef::new(RustPrimitive::Custom(
          "TestVariant".into(),
        ))]))
        .build(),
    ],
    serde_attrs: vec![],
    outer_attrs: vec![],
    case_insensitive: false,
    methods: vec![],
    ..Default::default()
  });

  if let RustType::Enum(e) = enum_def {
    assert!(e.methods.is_empty());
  }
}

#[test]
fn test_discriminator_deduplicates_same_type_mappings() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "InteractionSseEvent",
      json!({
        "oneOf": [
          { "$ref": "#/components/schemas/InteractionEvent" }
        ],
        "discriminator": {
          "propertyName": "type",
          "mapping": {
            "InteractionEvent": "#/components/schemas/InteractionEvent",
            "interaction_event": "#/components/schemas/InteractionEvent"
          }
        }
      }),
    ),
    (
      "InteractionEvent",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string" },
          "data": { "type": "string" }
        }
      }),
    ),
  ]));

  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("InteractionSseEvent", graph.get("InteractionSseEvent").unwrap())?;

  let RustType::DiscriminatedEnum(enum_def) = result.last().unwrap() else {
    panic!("Expected DiscriminatedEnum as last type")
  };

  assert_eq!(enum_def.name.to_string(), "InteractionSseEvent");
  assert_eq!(enum_def.discriminator_field, "type");

  assert_eq!(
    enum_def.variants.len(),
    1,
    "Expected 1 variant but got {}: {:?}",
    enum_def.variants.len(),
    enum_def.variants.iter().map(|v| &v.variant_name).collect::<Vec<_>>()
  );

  assert_eq!(enum_def.variants[0].type_name.base_type.to_string(), "InteractionEvent");

  Ok(())
}

#[test]
fn test_union_with_hyphenated_raw_name_converts_correctly() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "oneOf": [
      { "type": "string", "const": "option-a" },
      { "type": "string", "const": "option-b" }
    ]
  }));

  let result = union_converter.convert_union("my-test-enum", &schema);
  assert!(result.is_ok());

  let output = result.unwrap();
  let types = output.into_vec();

  let enum_def = types.iter().find_map(|t| match t {
    RustType::Enum(e) => Some(e),
    _ => None,
  });
  assert!(enum_def.is_some(), "should have an enum");

  let enum_def = enum_def.unwrap();
  assert_eq!(
    enum_def.name.to_string(),
    "MyTestEnum",
    "raw hyphenated name should convert to PascalCase"
  );
}

#[test]
fn test_union_with_underscored_raw_name_converts_correctly() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "oneOf": [
      { "type": "string", "const": "option-a" },
      { "type": "string", "const": "option-b" }
    ]
  }));

  let result = union_converter.convert_union("my_test_enum", &schema);
  assert!(result.is_ok());

  let output = result.unwrap();
  let types = output.into_vec();

  let enum_def = types.iter().find_map(|t| match t {
    RustType::Enum(e) => Some(e),
    _ => None,
  });
  assert!(enum_def.is_some(), "should have an enum");

  let enum_def = enum_def.unwrap();
  assert_eq!(
    enum_def.name.to_string(),
    "MyTestEnum",
    "raw underscored name should convert to PascalCase"
  );
}

#[test]
fn test_union_with_inline_struct_and_raw_name() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "my-union-type",
    json!({
      "oneOf": [
        {
          "type": "object",
          "properties": {
            "data_field": { "type": "string" }
          }
        }
      ]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("my-union-type", graph.get("my-union-type").unwrap())?;

  let binding = context.cache.borrow();
  let generated = &binding.types.types;
  let all_types = result.iter().chain(generated.iter()).collect::<Vec<&RustType>>();

  let enum_def = all_types.iter().find_map(|t| match t {
    RustType::Enum(e) => Some(e),
    _ => None,
  });
  assert!(enum_def.is_some(), "should produce an enum");
  assert_eq!(enum_def.unwrap().name.to_string(), "MyUnionType");

  let struct_def = all_types.iter().find_map(|t| match t {
    RustType::Struct(s) => Some(s),
    _ => None,
  });
  assert!(struct_def.is_some(), "should produce inline struct");

  let struct_name = struct_def.unwrap().name.to_string();
  assert!(
    struct_name.starts_with("MyUnionType"),
    "inline struct name '{struct_name}' should start with converted enum name 'MyUnionType'"
  );
  assert!(
    !struct_name.contains('-'),
    "struct name '{struct_name}' should not contain hyphens"
  );

  Ok(())
}

#[test]
fn test_already_pascalcase_name_not_double_converted() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "oneOf": [
      { "type": "string", "const": "OptionA" },
      { "type": "string", "const": "OptionB" }
    ]
  }));

  let result = union_converter.convert_union("MyPascalCaseEnum", &schema);
  assert!(result.is_ok());

  let output = result.unwrap();
  let types = output.into_vec();

  let enum_def = types.iter().find_map(|t| match t {
    RustType::Enum(e) => Some(e),
    _ => None,
  });
  assert!(enum_def.is_some());

  let enum_def = enum_def.unwrap();
  assert_eq!(
    enum_def.name.to_string(),
    "MyPascalCaseEnum",
    "already valid PascalCase name should remain unchanged"
  );
}

#[test]
fn test_relaxed_enum_with_raw_name() {
  let spec = serde_json::from_value::<oas3::Spec>(json!({
    "openapi": "3.1.0",
    "info": {
      "title": "Test",
      "version": "1.0.0"
    }
  }))
  .unwrap();

  let mut stats = GenerationStats::default();
  let registry = SchemaRegistry::new(&spec, &mut stats);

  let graph = Arc::new(registry);
  let context = create_test_context(graph.clone(), default_config());
  let union_converter = UnionConverter::new(context);

  let schema = parse_schema(json!({
    "anyOf": [
      { "type": "string", "const": "known-value" },
      { "type": "string" }
    ]
  }));

  let result = union_converter.convert_union("my-relaxed-enum", &schema);
  assert!(result.is_ok());

  let output = result.unwrap();
  let types = output.into_vec();

  let outer_enum = types.iter().find_map(|t| match t {
    RustType::Enum(e) if !e.name.to_string().ends_with(KNOWN_ENUM_VARIANT) => Some(e),
    _ => None,
  });
  assert!(outer_enum.is_some(), "should have outer wrapper enum");
  assert_eq!(
    outer_enum.unwrap().name.to_string(),
    "MyRelaxedEnum",
    "raw hyphenated name should convert to PascalCase"
  );

  let known_enum = types.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name.to_string().ends_with(KNOWN_ENUM_VARIANT) => Some(e),
    _ => None,
  });
  assert!(known_enum.is_some(), "should have known values enum");
  assert_eq!(
    known_enum.unwrap().name.to_string(),
    "MyRelaxedEnumKnown",
    "known enum should have converted name + Known suffix"
  );
}

#[test]
fn test_nested_anyof_with_null_flattens_to_single_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "CookingStrategy",
      json!({
        "anyOf": [
          {
            "description": "The cooking method configuration",
            "anyOf": [
              { "type": "string", "const": "auto" },
              { "$ref": "#/components/schemas/CustomCookingConfig" }
            ]
          },
          { "type": "null" }
        ]
      }),
    ),
    (
      "CustomCookingConfig",
      json!({
        "type": "object",
        "properties": {
          "mode": { "type": "string" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("CookingStrategy", graph.get("CookingStrategy").unwrap())?;

  let enum_def = result.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "CookingStrategy" => Some(e),
    _ => None,
  });

  assert!(enum_def.is_some(), "should produce CookingStrategy enum");
  let enum_def = enum_def.unwrap();

  assert_eq!(
    enum_def.variants.len(),
    2,
    "should have 2 variants (Auto and CustomCookingConfig), not a single Variant0 wrapper"
  );

  let variant_names = enum_def
    .variants
    .iter()
    .map(|v| v.name.to_string())
    .collect::<Vec<String>>();
  assert!(
    variant_names.contains(&"Auto".to_string()),
    "should have Auto variant, got {variant_names:?}"
  );
  assert!(
    variant_names.contains(&"CustomCookingConfig".to_string()),
    "should have CustomCookingConfig variant, got {variant_names:?}"
  );

  assert!(
    !variant_names.iter().any(|n| n.starts_with("Variant")),
    "should NOT have Variant0-style wrapper variants, got {variant_names:?}"
  );

  Ok(())
}

#[test]
fn test_nested_nullable_relaxed_anyof_produces_known_other_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "NullableRelaxed",
    json!({
      "anyOf": [
        {
          "anyOf": [
            { "type": "string", "const": "known1" },
            { "type": "string", "const": "known2" },
            { "type": "string" }
          ]
        },
        { "type": "null" }
      ]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("NullableRelaxed", graph.get("NullableRelaxed").unwrap())?;

  let outer_enum = result.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "NullableRelaxed" => Some(e),
    _ => None,
  });
  assert!(outer_enum.is_some(), "should produce NullableRelaxed enum");
  let outer_enum = outer_enum.unwrap();

  let variant_names = outer_enum
    .variants
    .iter()
    .map(|v| v.name.to_string())
    .collect::<Vec<_>>();
  assert!(
    variant_names.contains(&"Known".to_string()),
    "should have Known variant, got {variant_names:?}"
  );
  assert!(
    variant_names.contains(&"Other".to_string()),
    "should have Other variant, got {variant_names:?}"
  );

  let has_known_enum = result.iter().any(|t| match t {
    RustType::Enum(e) => e.name.to_string().ends_with("Known") && e.name != "NullableRelaxed",
    _ => false,
  });
  assert!(has_known_enum, "should generate inner known values enum");

  Ok(())
}

#[test]
fn test_nested_oneof_with_null_flattens_to_single_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "NestedUnion",
      json!({
        "anyOf": [
          {
            "oneOf": [
              { "type": "string", "const": "default" },
              { "$ref": "#/components/schemas/OptionA" }
            ]
          },
          { "type": "null" }
        ]
      }),
    ),
    (
      "OptionA",
      json!({
        "type": "object",
        "properties": {
          "value": { "type": "string" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("NestedUnion", graph.get("NestedUnion").unwrap())?;

  let enum_def = result.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "NestedUnion" => Some(e),
    _ => None,
  });

  assert!(enum_def.is_some(), "should produce NestedUnion enum");
  let enum_def = enum_def.unwrap();

  assert_eq!(
    enum_def.variants.len(),
    2,
    "should have 2 variants (Default and OptionA), not a single Variant0 wrapper"
  );

  let variant_names = enum_def
    .variants
    .iter()
    .map(|v| v.name.to_string())
    .collect::<Vec<String>>();
  assert!(
    variant_names.contains(&"Default".to_string()),
    "should have Default variant, got {variant_names:?}"
  );
  assert!(
    variant_names.contains(&"OptionA".to_string()),
    "should have OptionA variant, got {variant_names:?}"
  );

  Ok(())
}

#[test]
fn test_anyof_with_nullable_map_type_generates_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "ResponsePromptVariables",
      json!({
        "anyOf": [
          {
            "type": "object",
            "title": "Prompt Variables",
            "description": "Optional map of values to substitute",
            "additionalProperties": {
              "anyOf": [
                { "type": "string" },
                { "$ref": "#/components/schemas/InputContent" }
              ]
            }
          },
          { "type": "null" }
        ]
      }),
    ),
    (
      "InputContent",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("ResponsePromptVariables", graph.get("ResponsePromptVariables").unwrap())?;

  assert!(
    !result.is_empty(),
    "should generate types for anyOf with nullable map, not skip as single-variant wrapper"
  );

  let enum_def = result.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "ResponsePromptVariables" => Some(e),
    _ => None,
  });

  assert!(
    enum_def.is_some(),
    "should generate ResponsePromptVariables enum, got types: {:?}",
    result.iter().map(|t| t.type_name().to_string()).collect::<Vec<_>>()
  );

  let enum_def = enum_def.unwrap();
  assert_eq!(
    enum_def.variants.len(),
    1,
    "should have 1 variant (the map type, with null filtered out)"
  );

  let variant = &enum_def.variants[0];
  assert!(
    matches!(&variant.content, VariantContent::Tuple(types) if !types.is_empty()),
    "variant should have tuple content with the map type"
  );

  Ok(())
}

#[test]
fn test_anyof_with_string_enum_and_object_generates_inline_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "FunctionCall",
      json!({
        "description": "Controls which function is called by the model.",
        "anyOf": [
          {
            "type": "string",
            "description": "`none` or `auto` mode",
            "enum": ["none", "auto"],
            "title": "FunctionCallMode"
          },
          { "$ref": "#/components/schemas/FunctionCallOption" }
        ]
      }),
    ),
    (
      "FunctionCallOption",
      json!({
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The name of the function to call."
          }
        },
        "required": ["name"]
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("FunctionCall", graph.get("FunctionCall").unwrap())?;

  let binding = context.cache.borrow();
  let cached_types = &binding.types.types;
  let all_types = result.iter().chain(cached_types.iter()).collect::<Vec<_>>();

  let enum_def = all_types.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "FunctionCall" => Some(e),
    _ => None,
  });

  assert!(enum_def.is_some(), "should produce FunctionCall enum");
  let enum_def = enum_def.unwrap();

  assert_eq!(enum_def.variants.len(), 2, "should have 2 variants");

  let mode_variant = enum_def
    .variants
    .iter()
    .find(|v| v.name != "Option")
    .expect("should have a mode variant");

  match &mode_variant.content {
    VariantContent::Tuple(types) => {
      assert_eq!(types.len(), 1);
      let inner_type = types[0].to_rust_type();
      assert_ne!(
        inner_type, "String",
        "variant should NOT be plain String, should be an inline enum"
      );
    }
    VariantContent::Unit => panic!("expected tuple variant for mode variant"),
  }

  let inline_enum = all_types.iter().find_map(|t| match t {
    RustType::Enum(e)
      if e.name != "FunctionCall" && e.variants.iter().any(|v| v.name == "None" || v.name == "Auto") =>
    {
      Some(e)
    }
    _ => None,
  });

  assert!(
    inline_enum.is_some(),
    "should generate inline enum for string enum variant with None/Auto variants, types: {:?}",
    all_types.iter().map(|t| t.type_name().to_string()).collect::<Vec<_>>()
  );

  let inline_enum = inline_enum.unwrap();
  assert_eq!(inline_enum.variants.len(), 2);

  let variant_names = inline_enum
    .variants
    .iter()
    .map(|v| v.name.to_string())
    .collect::<Vec<_>>();
  assert!(variant_names.contains(&"None".to_string()), "should have None variant");
  assert!(variant_names.contains(&"Auto".to_string()), "should have Auto variant");

  Ok(())
}

#[test]
fn test_oneof_with_string_enum_variant_generates_inline_enum() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "ResponseFormat",
      json!({
        "oneOf": [
          {
            "type": "string",
            "title": "ResponseMode",
            "enum": ["streaming", "batch"]
          },
          { "$ref": "#/components/schemas/TextFormat" }
        ]
      }),
    ),
    (
      "TextFormat",
      json!({
        "type": "object",
        "properties": {
          "type": { "type": "string", "const": "text" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("ResponseFormat", graph.get("ResponseFormat").unwrap())?;

  let binding = context.cache.borrow();
  let cached_types = &binding.types.types;
  let all_types = result.iter().chain(cached_types.iter()).collect::<Vec<_>>();

  let enum_def = all_types.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "ResponseFormat" => Some(e),
    _ => None,
  });

  assert!(enum_def.is_some(), "should produce ResponseFormat enum");
  let enum_def = enum_def.unwrap();

  let mode_variant = enum_def
    .variants
    .iter()
    .find(|v| v.name != "TextFormat")
    .expect("should have a mode variant");

  match &mode_variant.content {
    VariantContent::Tuple(types) => {
      assert_eq!(types.len(), 1);
      let inner_type = types[0].to_rust_type();
      assert_ne!(
        inner_type, "String",
        "variant should NOT be plain String, should be an inline enum"
      );
    }
    VariantContent::Unit => panic!("expected tuple variant for mode variant"),
  }

  let inline_enum = all_types.iter().find_map(|t| match t {
    RustType::Enum(e)
      if e.name != "ResponseFormat" && e.variants.iter().any(|v| v.name == "Streaming" || v.name == "Batch") =>
    {
      Some(e)
    }
    _ => None,
  });

  assert!(
    inline_enum.is_some(),
    "should generate inline enum with Streaming/Batch variants, types: {:?}",
    all_types.iter().map(|t| t.type_name().to_string()).collect::<Vec<_>>()
  );

  let inline_enum = inline_enum.unwrap();
  let variant_names = inline_enum
    .variants
    .iter()
    .map(|v| v.name.to_string())
    .collect::<Vec<_>>();
  assert!(variant_names.contains(&"Streaming".to_string()));
  assert!(variant_names.contains(&"Batch".to_string()));

  Ok(())
}

#[test]
fn test_anyof_with_single_value_enum_uses_primitive() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "anyOf": [
          {
            "type": "string",
            "enum": ["auto"]
          },
          { "$ref": "#/components/schemas/ObjectVariant" }
        ]
      }),
    ),
    (
      "ObjectVariant",
      json!({
        "type": "object",
        "properties": {
          "name": { "type": "string" }
        }
      }),
    ),
  ]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);

  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let enum_def = result.iter().find_map(|t| match t {
    RustType::Enum(e) if e.name == "TestUnion" => Some(e),
    _ => None,
  });

  assert!(enum_def.is_some(), "should produce TestUnion enum");
  let enum_def = enum_def.unwrap();

  let string_variant = enum_def
    .variants
    .iter()
    .find(|v| v.name != "ObjectVariant")
    .expect("should have a non-ObjectVariant variant");

  match &string_variant.content {
    VariantContent::Tuple(types) => {
      assert_eq!(types.len(), 1);
      let inner_type = types[0].to_rust_type();
      assert_eq!(
        inner_type, "String",
        "single-value enum should use primitive String type"
      );
    }
    VariantContent::Unit => panic!("expected tuple variant"),
  }

  Ok(())
}

#[test]
fn test_sorted_layout_value_enum_sorts_variants_alphabetically() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "Status",
    json!({
      "type": "string",
      "enum": ["zeta", "alpha", "gamma", "beta"]
    }),
  )]));
  let context = create_test_context(graph.clone(), config_with_sorted_enums());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Status", graph.get("Status").unwrap())?;

  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  let names = enum_def
    .variants
    .iter()
    .map(|v| v.name.as_str().to_string())
    .collect::<Vec<_>>();
  assert_eq!(names, vec!["Alpha", "Beta", "Gamma", "Zeta"]);
  Ok(())
}

#[test]
fn test_default_layout_value_enum_preserves_spec_order() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![(
    "Status",
    json!({
      "type": "string",
      "enum": ["zeta", "alpha", "gamma", "beta"]
    }),
  )]));
  let context = create_test_context(graph.clone(), default_config());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Status", graph.get("Status").unwrap())?;

  let RustType::Enum(enum_def) = &result[0] else {
    panic!("Expected enum")
  };

  let names = enum_def
    .variants
    .iter()
    .map(|v| v.name.as_str().to_string())
    .collect::<Vec<_>>();
  assert_eq!(names, vec!["Zeta", "Alpha", "Gamma", "Beta"]);
  Ok(())
}

#[test]
fn test_sorted_layout_value_enum_permutation_invariance() -> anyhow::Result<()> {
  let permutations = [
    vec!["alpha", "beta", "gamma"],
    vec!["beta", "alpha", "gamma"],
    vec!["gamma", "beta", "alpha"],
  ];

  let mut emitted_orders = vec![];
  for variants in permutations {
    let graph = create_test_graph(parse_schemas(vec![(
      "Status",
      json!({
        "type": "string",
        "enum": variants
      }),
    )]));
    let context = create_test_context(graph.clone(), config_with_sorted_enums());
    let converter = SchemaConverter::new(&context);
    let result = converter.convert_schema("Status", graph.get("Status").unwrap())?;
    let RustType::Enum(enum_def) = &result[0] else {
      panic!("Expected enum")
    };
    emitted_orders.push(
      enum_def
        .variants
        .iter()
        .map(|v| v.name.as_str().to_string())
        .collect::<Vec<_>>(),
    );
  }

  let expected = vec!["Alpha".to_string(), "Beta".to_string(), "Gamma".to_string()];
  for order in &emitted_orders {
    assert_eq!(order, &expected, "all permutations should produce identical output");
  }
  Ok(())
}

#[test]
fn test_sorted_layout_union_variants_sorted_alphabetically() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "oneOf": [
          { "$ref": "#/components/schemas/Zebra" },
          { "$ref": "#/components/schemas/Apple" },
          { "$ref": "#/components/schemas/Mango" }
        ]
      }),
    ),
    (
      "Zebra",
      json!({ "type": "object", "properties": { "stripes": { "type": "integer" } } }),
    ),
    (
      "Apple",
      json!({ "type": "object", "properties": { "color": { "type": "string" } } }),
    ),
    (
      "Mango",
      json!({ "type": "object", "properties": { "ripe": { "type": "boolean" } } }),
    ),
  ]));
  let context = create_test_context(graph.clone(), config_with_sorted_enums());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let enum_def = result
    .iter()
    .find_map(|t| match t {
      RustType::Enum(e) if e.name == EnumToken::new("TestUnion") => Some(e),
      _ => None,
    })
    .expect("TestUnion enum should exist");

  let names = enum_def
    .variants
    .iter()
    .map(|v| v.name.as_str().to_string())
    .collect::<Vec<_>>();
  assert_eq!(names, vec!["Apple", "Mango", "Zebra"]);
  Ok(())
}

#[test]
fn test_sorted_layout_discriminated_enum_sorts_variants() -> anyhow::Result<()> {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "TestUnion",
      json!({
        "oneOf": [
          { "$ref": "#/components/schemas/Zeta" },
          { "$ref": "#/components/schemas/Alpha" },
          { "$ref": "#/components/schemas/Mango" }
        ],
        "discriminator": {
          "propertyName": "kind",
          "mapping": {
            "zeta": "#/components/schemas/Zeta",
            "alpha": "#/components/schemas/Alpha",
            "mango": "#/components/schemas/Mango"
          }
        }
      }),
    ),
    (
      "Zeta",
      json!({ "type": "object", "properties": { "kind": { "type": "string", "const": "zeta" } } }),
    ),
    (
      "Alpha",
      json!({ "type": "object", "properties": { "kind": { "type": "string", "const": "alpha" } } }),
    ),
    (
      "Mango",
      json!({ "type": "object", "properties": { "kind": { "type": "string", "const": "mango" } } }),
    ),
  ]));
  let context = create_test_context(graph.clone(), config_with_sorted_enums());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("TestUnion", graph.get("TestUnion").unwrap())?;

  let RustType::DiscriminatedEnum(enum_def) = result.last().unwrap() else {
    panic!("Expected DiscriminatedEnum as last type")
  };

  let names = enum_def
    .variants
    .iter()
    .map(|v| v.variant_name.as_str().to_string())
    .collect::<Vec<_>>();
  assert_eq!(names, vec!["Alpha", "Mango", "Zeta"]);
  Ok(())
}
