use std::{
  collections::BTreeMap,
  hash::{DefaultHasher, Hash, Hasher},
  sync::Arc,
};

use oas3::spec::ObjectSchema;
use serde_json::json;

use super::support::assert_has_known_variant;
use crate::{
  generator::{
    ast::{EnumDef, EnumToken, EnumVariantToken, RustType, VariantContent},
    converter::{
      SchemaConverter, cache::SharedSchemaCache, hashing::CanonicalSchema, type_resolver::TypeResolver,
      union_types::variants_to_cache_key, unions::UnionConverter,
    },
    naming::constants::KNOWN_ENUM_VARIANT,
    schema_registry::SchemaRegistry,
  },
  tests::common::{
    create_test_context, create_test_graph, create_test_spec, default_config, parse_schema, parse_schemas,
  },
  utils::SchemaExt,
};

fn make_enum_cache_key(schema: &ObjectSchema) -> Option<Vec<String>> {
  let spec = create_test_spec(BTreeMap::new());
  let variants = schema.extract_enum_entries(&spec);
  (!variants.is_empty()).then(|| variants_to_cache_key(&variants))
}

fn create_test_converter(graph: &Arc<SchemaRegistry>) -> SchemaConverter {
  let context = create_test_context(graph.clone(), default_config());
  SchemaConverter::new(&context)
}

#[test]
fn test_canonical_schema_equality_and_ordering() {
  let schema1 = parse_schema(json!({
    "required": ["name", "tag_id"]
  }));

  let schema2 = parse_schema(json!({
    "required": ["tag_id", "name"]
  }));

  let schema3 = parse_schema(json!({
    "required": ["different"]
  }));

  let first = CanonicalSchema::from_schema(&schema1).expect("should succeed");
  let repeated = CanonicalSchema::from_schema(&schema1).expect("should succeed");
  let reordered = CanonicalSchema::from_schema(&schema2).expect("should succeed");
  let different = CanonicalSchema::from_schema(&schema3).expect("should succeed");

  assert_eq!(first, repeated, "CanonicalSchema should be deterministic across calls");
  assert_eq!(
    first, reordered,
    "Required array order should not affect equality due to RFC 8785 canonicalization"
  );
  assert_ne!(
    first, different,
    "Different schemas should produce different CanonicalSchemas"
  );

  assert!(first <= reordered, "Equal schemas should satisfy <= ordering");
  assert!(first >= reordered, "Equal schemas should satisfy >= ordering");

  let mut schemas_diff = [different.clone(), first.clone()];
  schemas_diff.sort();
  assert_eq!(schemas_diff.len(), 2, "Sorting should preserve all elements");
}

#[test]
fn test_relaxed_enum_generates_known_variant() {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "PembrokeEnum",
      json!({
        "type": "string",
        "enum": ["bark", "sploot", "loaf"]
      }),
    ),
    (
      "SplootEnum",
      json!({
        "anyOf": [
          { "type": "string" },
          { "$ref": "#/components/schemas/PembrokeEnum" }
        ]
      }),
    ),
  ]));

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

  let sploot_schema = graph.get("SplootEnum").unwrap();
  let optimized_output = union_converter
    .convert_union("SplootEnum", sploot_schema)
    .expect("Should convert anyOf union");

  let optimized_result = optimized_output.into_vec();
  assert!(!optimized_result.is_empty(), "Should generate at least one type");

  let outer_enum = optimized_result
    .iter()
    .find(|t| matches!(t, RustType::Enum(e) if e.name == "SplootEnum"));
  assert!(outer_enum.is_some(), "Should generate SplootEnum");

  if let Some(RustType::Enum(e)) = outer_enum {
    assert_has_known_variant(e);
  }
}

#[test]
fn test_relaxed_enum_with_ref() {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "BarkModel",
      json!({
        "type": "string",
        "enum": ["corgi-v1", "corgi-v2"]
      }),
    ),
    (
      "FloofIds",
      json!({
        "anyOf": [
          { "type": "string" },
          { "$ref": "#/components/schemas/BarkModel" }
        ]
      }),
    ),
  ]));

  let converter = create_test_converter(&graph);

  let bark_model_result = converter
    .convert_schema("BarkModel", graph.get("BarkModel").unwrap())
    .expect("Should convert BarkModel");
  assert_eq!(bark_model_result.len(), 1);

  let model_ids_result = converter
    .convert_schema("FloofIds", graph.get("FloofIds").unwrap())
    .expect("Should convert FloofIds");

  assert!(!model_ids_result.is_empty(), "Should generate at least one type");

  let outer_enum = model_ids_result
    .iter()
    .find(|t| matches!(t, RustType::Enum(e) if e.name == "FloofIds"));
  assert!(outer_enum.is_some(), "Should generate FloofIds enum");

  if let Some(RustType::Enum(outer)) = outer_enum {
    assert_has_known_variant(outer);
  }
}

#[test]
fn test_name_uniqueness() {
  let mut cache = SharedSchemaCache::new();

  cache.mark_name_used("Corgi".to_string());
  let unique_name = cache.make_unique_name("Corgi");
  assert_ne!(
    unique_name, "Corgi",
    "Should generate unique name when name is already used"
  );
  assert!(unique_name.starts_with("Corgi"), "Should maintain base name prefix");

  cache.mark_name_used("Nugget".to_string());
  let name1 = cache.make_unique_name("Nugget");
  cache.mark_name_used(name1.clone());
  let name2 = cache.make_unique_name("Nugget");
  cache.mark_name_used(name2.clone());
  let name3 = cache.make_unique_name("Nugget");

  let unique_names = [&name1, &name2, &name3];
  for (i, current) in unique_names.iter().enumerate() {
    assert!(current.starts_with("Nugget"), "Name {i} should maintain base name");
    for (j, other) in unique_names.iter().enumerate() {
      if i != j {
        assert_ne!(current, other, "Names {i} and {j} should be different");
      }
    }
  }
}

#[test]
fn test_precomputed_names() {
  let schema = parse_schema(json!({
    "required": ["tag_id"]
  }));

  let canonical = CanonicalSchema::from_schema(&schema).expect("should succeed");
  let mut precomputed_names = BTreeMap::new();
  precomputed_names.insert(canonical, "FloofName".to_string());

  let enum_values = vec!["bark".to_string(), "sploot".to_string()];
  let mut precomputed_enum_names = BTreeMap::new();
  precomputed_enum_names.insert(enum_values.clone(), "CardiganEnum".to_string());

  let mut cache = SharedSchemaCache::new();
  cache.set_precomputed_names(precomputed_names, precomputed_enum_names, BTreeMap::new());

  let preferred_name = cache
    .get_preferred_name(&schema, "DefaultName")
    .expect("should get preferred name");
  assert_eq!(preferred_name, "FloofName", "Should use precomputed schema name");

  let found_enum_name = cache.get_enum_name(&enum_values);
  assert_eq!(
    found_enum_name,
    Some("CardiganEnum".to_string()),
    "Should find precomputed enum name"
  );
}

#[test]
fn test_cache_operations() {
  let mut cache = SharedSchemaCache::new();

  let enum_values = vec!["brown".to_string(), "white".to_string(), "black".to_string()];
  assert!(
    !cache.is_enum_generated(&enum_values),
    "Enum should not be generated initially"
  );
  cache.register_enum(enum_values.clone(), "Howl".to_string());
  assert!(
    cache.is_enum_generated(&enum_values),
    "Enum should be marked as generated"
  );
  assert_eq!(
    cache.get_enum_name(&enum_values),
    Some("Howl".to_string()),
    "Should retrieve registered enum name"
  );

  let new_schema = parse_schema(json!({
    "required": ["name"]
  }));
  let result = cache.get_type_name(&new_schema).expect("should succeed");
  assert_eq!(result, None, "Should return None for uncached schema");

  let schema1 = parse_schema(json!({
    "type": "string",
    "enum": ["a", "b"]
  }));
  let schema2 = parse_schema(json!({
    "type": "string",
    "enum": ["x", "y"]
  }));

  let enum1 = RustType::Enum(EnumDef {
    name: EnumToken::new("FirstEnum"),
    variants: vec![],
    serde_attrs: vec![],
    outer_attrs: vec![],
    case_insensitive: false,
    methods: vec![],
    ..Default::default()
  });

  let enum2 = RustType::Enum(EnumDef {
    name: EnumToken::new("SecondEnum"),
    variants: vec![],
    serde_attrs: vec![],
    outer_attrs: vec![],
    case_insensitive: false,
    methods: vec![],
    ..Default::default()
  });

  let type_cache = SharedSchemaCache::new();

  let reg1 = type_cache
    .prepare_registration(&schema1, "FirstEnum", make_enum_cache_key(&schema1))
    .expect("Should prepare first enum");
  let named_enum1 = SharedSchemaCache::apply_name_to_type(enum1, &reg1.assigned_name);
  let mut type_cache = type_cache;
  type_cache.commit_registration(reg1, vec![], named_enum1);

  let reg2 = type_cache
    .prepare_registration(&schema2, "SecondEnum", make_enum_cache_key(&schema2))
    .expect("Should prepare second enum");
  let named_enum2 = SharedSchemaCache::apply_name_to_type(enum2, &reg2.assigned_name);
  type_cache.commit_registration(reg2, vec![], named_enum2);

  let types = type_cache.take_types();
  assert_eq!(types.len(), 2, "Should return all generated types");
}

#[test]
fn test_canonical_schema_as_btreemap_key() {
  let schema_a = parse_schema(json!({
    "required": ["alpha"]
  }));
  let schema_b = parse_schema(json!({
    "required": ["beta"]
  }));
  let schema_a_reordered = parse_schema(json!({
    "required": ["alpha"]
  }));

  let canonical_a = CanonicalSchema::from_schema(&schema_a).expect("should succeed");
  let canonical_b = CanonicalSchema::from_schema(&schema_b).expect("should succeed");
  let canonical_a_dup = CanonicalSchema::from_schema(&schema_a_reordered).expect("should succeed");

  let mut map: BTreeMap<CanonicalSchema, &str> = BTreeMap::new();
  map.insert(canonical_a.clone(), "first");
  map.insert(canonical_b.clone(), "second");

  assert_eq!(map.len(), 2, "Map should have two entries for different schemas");
  assert_eq!(map.get(&canonical_a), Some(&"first"));
  assert_eq!(map.get(&canonical_b), Some(&"second"));
  assert_eq!(
    map.get(&canonical_a_dup),
    Some(&"first"),
    "Lookup with equivalent schema should find same entry"
  );

  map.insert(canonical_a_dup, "overwritten");
  assert_eq!(map.len(), 2, "Map size should remain 2 after inserting duplicate key");
  assert_eq!(
    map.get(&canonical_a),
    Some(&"overwritten"),
    "Value should be overwritten for equivalent key"
  );
}

#[test]
fn test_canonical_schema_preserves_enum_order() {
  let schema1 = parse_schema(json!({
    "enum": ["z", "a", "m"]
  }));
  let schema2 = parse_schema(json!({
    "enum": ["a", "m", "z"]
  }));

  let canonical1 = CanonicalSchema::from_schema(&schema1).expect("should succeed");
  let canonical2 = CanonicalSchema::from_schema(&schema2).expect("should succeed");

  assert_ne!(
    canonical1, canonical2,
    "Enum value order should affect canonical equality"
  );
}

#[test]
fn test_canonical_schema_normalizes_type_array_order() {
  let schema1 = parse_schema(json!({
    "type": ["string", "null"]
  }));
  let schema2 = parse_schema(json!({
    "type": ["null", "string"]
  }));

  let canonical1 = CanonicalSchema::from_schema(&schema1).expect("should succeed");
  let canonical2 = CanonicalSchema::from_schema(&schema2).expect("should succeed");

  assert_eq!(
    canonical1, canonical2,
    "Type array order should not affect canonical equality"
  );
}

#[test]
fn test_canonical_schema_hash_consistency() {
  let schema = parse_schema(json!({
    "required": ["a", "b"]
  }));

  let canonical = CanonicalSchema::from_schema(&schema).expect("should succeed");

  let mut hasher1 = DefaultHasher::new();
  canonical.hash(&mut hasher1);
  let hash1 = hasher1.finish();

  let mut hasher2 = DefaultHasher::new();
  canonical.hash(&mut hasher2);
  let hash2 = hasher2.finish();

  assert_eq!(hash1, hash2, "Hash should be consistent across calls");

  let canonical_dup = CanonicalSchema::from_schema(&schema).expect("should succeed");
  let mut hasher3 = DefaultHasher::new();
  canonical_dup.hash(&mut hasher3);
  let hash3 = hasher3.finish();

  assert_eq!(hash1, hash3, "Equal CanonicalSchemas should produce equal hashes");
}

#[test]
fn test_relaxed_enum_does_not_overwrite_inner_enum_registration() {
  let graph = create_test_graph(parse_schemas(vec![
    (
      "PembrokeEnum",
      json!({
        "type": "string",
        "enum": ["waddle", "sploot", "loaf", "zoom"]
      }),
    ),
    (
      "FloofRelaxed",
      json!({
        "anyOf": [
          { "type": "string" },
          { "$ref": "#/components/schemas/PembrokeEnum" }
        ]
      }),
    ),
    (
      "SplootRelaxed",
      json!({
        "anyOf": [
          { "type": "string" },
          { "$ref": "#/components/schemas/PembrokeEnum" }
        ]
      }),
    ),
  ]));

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

  let floof_schema = graph.get("FloofRelaxed").unwrap();
  let first_output = union_converter
    .convert_union("FloofRelaxed", floof_schema)
    .expect("Should convert first anyOf union");
  let first_result = first_output.into_vec();

  let first_outer = first_result
    .iter()
    .find(|t| matches!(t, RustType::Enum(e) if e.name == "FloofRelaxed"))
    .expect("Should generate FloofRelaxed");

  let inner_enum_name = if let RustType::Enum(e) = first_outer {
    let known_variant = e
      .variants
      .iter()
      .find(|v| v.name == EnumVariantToken::new(KNOWN_ENUM_VARIANT))
      .expect("Should have Known variant");
    if let VariantContent::Tuple(refs) = &known_variant.content {
      refs[0].base_type.to_string()
    } else {
      panic!("Known variant should have tuple content");
    }
  } else {
    panic!("FloofRelaxed should be an enum");
  };

  let sploot_schema = graph.get("SplootRelaxed").unwrap();
  let second_output = union_converter
    .convert_union("SplootRelaxed", sploot_schema)
    .expect("Should convert second anyOf union");
  let second_result = second_output.into_vec();

  let second_outer = second_result
    .iter()
    .find(|t| matches!(t, RustType::Enum(e) if e.name == "SplootRelaxed"))
    .expect("Should generate SplootRelaxed");

  if let RustType::Enum(e) = second_outer {
    let known_variant = e
      .variants
      .iter()
      .find(|v| v.name == EnumVariantToken::new(KNOWN_ENUM_VARIANT))
      .expect("Should have Known variant");
    if let VariantContent::Tuple(refs) = &known_variant.content {
      let second_inner_name = refs[0].base_type.to_string();
      assert_eq!(
        inner_enum_name, second_inner_name,
        "Both relaxed enums should reference the same inner known values enum. \
        First references '{inner_enum_name}', second references '{second_inner_name}'. \
        This is a regression of issue #57."
      );
    } else {
      panic!("Known variant should have tuple content");
    }
  } else {
    panic!("SplootRelaxed should be an enum");
  }
}

#[test]
fn test_canonical_schema_with_large_numbers_succeeds() {
  let schema = serde_json::from_str::<ObjectSchema>(
    r#"{
    "minimum": -9223372036854776000,
    "maximum": 9223372036854776000
  }"#,
  )
  .unwrap();

  let result = CanonicalSchema::from_schema(&schema);
  assert!(
    result.is_ok(),
    "Should successfully canonicalize schema with large numbers (exceeding IEEE 754 safe range)"
  );
}

#[test]
fn test_canonical_schema_large_numbers_are_normalized() {
  let schema1 = serde_json::from_str::<ObjectSchema>(
    r#"{
    "minimum": -9223372036854776000,
    "maximum": 9223372036854776000
  }"#,
  )
  .unwrap();

  let schema2 = serde_json::from_str::<ObjectSchema>(
    r#"{
    "minimum": -9999999999999999999,
    "maximum": 9999999999999999999
  }"#,
  )
  .unwrap();

  let canonical1 = CanonicalSchema::from_schema(&schema1).expect("should succeed");
  let canonical2 = CanonicalSchema::from_schema(&schema2).expect("should succeed");

  assert_eq!(
    canonical1, canonical2,
    "Large numbers outside IEEE 754 safe range should be clamped to the same value"
  );
}

#[test]
fn test_get_generated_enum_name_returns_none_for_precomputed_only() {
  let enum_values = vec!["bark".to_string(), "sploot".to_string()];
  let mut precomputed_enum_names = BTreeMap::new();
  precomputed_enum_names.insert(enum_values.clone(), "CardiganEnum".to_string());

  let mut cache = SharedSchemaCache::new();
  cache.set_precomputed_names(BTreeMap::new(), precomputed_enum_names, BTreeMap::new());

  assert_eq!(
    cache.get_enum_name(&enum_values),
    Some("CardiganEnum".to_string()),
    "get_enum_name should return precomputed name"
  );

  assert_eq!(
    cache.get_generated_enum_name(&enum_values),
    None,
    "get_generated_enum_name should return None when enum is only precomputed, not registered"
  );
}

#[test]
fn test_get_generated_enum_name_returns_name_when_registered() {
  let enum_values = vec!["brown".to_string(), "white".to_string()];

  let mut cache = SharedSchemaCache::new();

  assert_eq!(
    cache.get_generated_enum_name(&enum_values),
    None,
    "Should return None before registration"
  );

  cache.register_enum(enum_values.clone(), "HowlEnum".to_string());

  assert_eq!(
    cache.get_generated_enum_name(&enum_values),
    Some("HowlEnum".to_string()),
    "Should return name after registration"
  );
}
