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

use oas3::spec::{ObjectOrReference, ObjectSchema, Schema, SchemaType, SchemaTypeSet};

use crate::{
  generator::{
    ast::{OuterAttr, RustPrimitive, RustType, SerdeAsFieldAttr},
    converter::{CodegenConfig, SchemaConverter, fields::FieldConverter},
  },
  tests::common::{create_test_context, create_test_graph, make_field},
};

fn config_with_customizations(customizations: HashMap<String, String>) -> CodegenConfig {
  CodegenConfig {
    customizations,
    ..Default::default()
  }
}

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

#[test]
fn test_datetime_field_with_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "sploot_at".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date-time".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["sploot_at".to_string()];

  let customizations = HashMap::from([("date_time".to_string(), "crate::MyDateTime".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "sploot_at")
    .expect("sploot_at field should exist");

  assert!(
    field.serde_as_attr.is_some(),
    "Field should have serde_as_attr when customization is provided"
  );

  let serde_as = field.serde_as_attr.as_ref().unwrap();
  assert_eq!(
    *serde_as,
    SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyDateTime".to_string(),
      optional: false,
      is_array: false,
    }
  );

  assert!(
    struct_def.outer_attrs.contains(&OuterAttr::SerdeAs),
    "Struct should have #[serde_with::serde_as] outer attribute"
  );

  Ok(())
}

#[test]
fn test_optional_datetime_field_with_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "waddle_at".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date-time".to_string()),
      ..Default::default()
    }),
  );

  let customizations = HashMap::from([("date_time".to_string(), "crate::MyDateTime".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "waddle_at")
    .expect("waddle_at field should exist");

  let serde_as = field.serde_as_attr.as_ref().expect("Should have serde_as_attr");
  assert_eq!(
    *serde_as,
    SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyDateTime".to_string(),
      optional: true,
      is_array: false,
    }
  );

  Ok(())
}

#[test]
fn test_array_of_datetime_with_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "toebeans".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::Array)),
      items: Some(Box::new(Schema::Object(Box::new(ObjectOrReference::Object(
        ObjectSchema {
          schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
          format: Some("date-time".to_string()),
          ..Default::default()
        },
      ))))),
      ..Default::default()
    }),
  );
  schema.required = vec!["toebeans".to_string()];

  let customizations = HashMap::from([("date_time".to_string(), "crate::MyDateTime".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "toebeans")
    .expect("toebeans field should exist");

  let serde_as = field.serde_as_attr.as_ref().expect("Should have serde_as_attr");
  assert_eq!(
    *serde_as,
    SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyDateTime".to_string(),
      optional: false,
      is_array: true,
    }
  );

  Ok(())
}

#[test]
fn test_date_field_with_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "corgi_date".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["corgi_date".to_string()];

  let customizations = HashMap::from([("date".to_string(), "crate::MyDate".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Corgi".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Corgi", graph.get("Corgi").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "corgi_date")
    .expect("corgi_date field should exist");

  let serde_as = field.serde_as_attr.as_ref().expect("Should have serde_as_attr");
  assert_eq!(
    *serde_as,
    SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyDate".to_string(),
      optional: false,
      is_array: false,
    }
  );

  Ok(())
}

#[test]
fn test_uuid_field_with_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "tag_id".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("uuid".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["tag_id".to_string()];

  let customizations = HashMap::from([("uuid".to_string(), "crate::MyUuid".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Cardigan".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Cardigan", graph.get("Cardigan").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "tag_id")
    .expect("tag_id field should exist");

  let serde_as = field.serde_as_attr.as_ref().expect("Should have serde_as_attr");
  assert_eq!(
    *serde_as,
    SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyUuid".to_string(),
      optional: false,
      is_array: false,
    }
  );

  Ok(())
}

#[test]
fn test_no_serde_as_attr_without_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "sploot_at".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date-time".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["sploot_at".to_string()];

  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), CodegenConfig::default());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "sploot_at")
    .expect("sploot_at field should exist");

  assert!(
    field.serde_as_attr.is_none(),
    "Field should not have serde_as_attr when no customization is provided"
  );

  assert!(
    !struct_def.outer_attrs.contains(&OuterAttr::SerdeAs),
    "Struct should not have #[serde_with::serde_as] when no fields have serde_as_attr"
  );

  Ok(())
}

#[test]
fn test_byte_format_emits_base64_serde_as() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "payload".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("byte".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["payload".to_string()];

  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), CodegenConfig::default());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "payload")
    .expect("payload field should exist");

  assert_eq!(field.rust_type.base_type, RustPrimitive::Bytes);
  assert_eq!(
    field.serde_as_attr.as_ref().expect("Should have serde_as_attr"),
    &SerdeAsFieldAttr::CustomOverride {
      custom_type: "serde_with::base64::Base64".to_string(),
      optional: false,
      is_array: false,
    }
  );
  assert!(struct_def.outer_attrs.contains(&OuterAttr::SerdeAs));

  Ok(())
}

#[test]
fn test_binary_format_does_not_emit_base64() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "payload".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("binary".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["payload".to_string()];

  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), CodegenConfig::default());
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "payload")
    .expect("payload field should exist");

  assert_eq!(field.rust_type.base_type, RustPrimitive::Bytes);
  assert!(
    field.serde_as_attr.is_none(),
    "format: binary should not emit a base64 serde_as attribute"
  );

  Ok(())
}

#[test]
fn test_byte_format_customization_overrides_base64() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "payload".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("byte".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["payload".to_string()];

  let customizations = HashMap::from([("byte".to_string(), "crate::MyBase64".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Frappe".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Frappe", graph.get("Frappe").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "payload")
    .expect("payload field should exist");

  assert_eq!(
    field.serde_as_attr.as_ref().expect("Should have serde_as_attr"),
    &SerdeAsFieldAttr::CustomOverride {
      custom_type: "crate::MyBase64".to_string(),
      optional: false,
      is_array: false,
    }
  );

  Ok(())
}

#[test]
fn field_name_deduplication() {
  struct Case {
    name: &'static str,
    fields: Vec<(&'static str, bool)>,
    expected_names: Vec<&'static str>,
  }

  let cases = [
    Case {
      name: "no duplicates",
      fields: vec![("loaf", false), ("sploot", false), ("floof", false)],
      expected_names: vec!["loaf", "sploot", "floof"],
    },
    Case {
      name: "empty input",
      fields: vec![],
      expected_names: vec![],
    },
    Case {
      name: "all non-deprecated renamed with suffix",
      fields: vec![("loaf", false), ("loaf", false), ("loaf", false)],
      expected_names: vec!["loaf", "loaf_2", "loaf_3"],
    },
    Case {
      name: "deprecated removed when mixed with non-deprecated",
      fields: vec![("loaf", true), ("loaf", false), ("sploot", false)],
      expected_names: vec!["loaf", "sploot"],
    },
    Case {
      name: "all deprecated renamed with suffix",
      fields: vec![("loaf", true), ("loaf", true)],
      expected_names: vec!["loaf", "loaf_2"],
    },
    Case {
      name: "multiple groups",
      fields: vec![("loaf", false), ("sploot", true), ("loaf", false), ("sploot", false)],
      expected_names: vec!["loaf", "loaf_2", "sploot"],
    },
  ];

  for case in cases {
    let fields = case.fields.iter().map(|(n, d)| make_field(n, *d)).collect::<Vec<_>>();
    let result = FieldConverter::deduplicate_names(fields);
    let names = result.iter().map(|f| f.name.as_str()).collect::<Vec<_>>();

    assert_eq!(names.len(), case.expected_names.len(), "{}: length mismatch", case.name);

    for expected in &case.expected_names {
      assert!(names.contains(expected), "{}: missing '{}'", case.name, expected);
    }
  }
}

#[test]
fn test_string_field_no_customization() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "name".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      ..Default::default()
    }),
  );
  schema.required = vec!["name".to_string()];

  let customizations = HashMap::from([("date_time".to_string(), "crate::MyDateTime".to_string())]);
  let graph = create_test_graph(BTreeMap::from([("Corgi".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Corgi", graph.get("Corgi").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "name")
    .expect("name field should exist");

  assert_eq!(field.rust_type.base_type, RustPrimitive::String);
  assert!(
    field.serde_as_attr.is_none(),
    "String fields should not be customized by date_time customization"
  );

  Ok(())
}

#[test]
fn test_multiple_customizations() -> anyhow::Result<()> {
  let mut schema = ObjectSchema {
    schema_type: Some(SchemaTypeSet::Single(SchemaType::Object)),
    ..Default::default()
  };
  schema.properties.insert(
    "sploot_at".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date-time".to_string()),
      ..Default::default()
    }),
  );
  schema.properties.insert(
    "corgi_date".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("date".to_string()),
      ..Default::default()
    }),
  );
  schema.properties.insert(
    "tag_id".to_string(),
    object_schema(ObjectSchema {
      schema_type: Some(SchemaTypeSet::Single(SchemaType::String)),
      format: Some("uuid".to_string()),
      ..Default::default()
    }),
  );
  schema.required = vec!["sploot_at".to_string(), "corgi_date".to_string(), "tag_id".to_string()];

  let customizations = HashMap::from([
    ("date_time".to_string(), "crate::MyDateTime".to_string()),
    ("date".to_string(), "crate::MyDate".to_string()),
    ("uuid".to_string(), "crate::MyUuid".to_string()),
  ]);

  let graph = create_test_graph(BTreeMap::from([("Cardigan".to_string(), schema)]));
  let context = create_test_context(graph.clone(), config_with_customizations(customizations));
  let converter = SchemaConverter::new(&context);
  let result = converter.convert_schema("Cardigan", graph.get("Cardigan").unwrap())?;

  let struct_def = result
    .iter()
    .find_map(|ty| match ty {
      RustType::Struct(def) => Some(def),
      _ => None,
    })
    .expect("Struct should be present");

  let sploot_field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "sploot_at")
    .expect("sploot_at field should exist");
  assert!(matches!(
    sploot_field.serde_as_attr,
    Some(SerdeAsFieldAttr::CustomOverride { ref custom_type, .. }) if custom_type == "crate::MyDateTime"
  ));

  let birth_field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "corgi_date")
    .expect("corgi_date field should exist");
  assert!(matches!(
    birth_field.serde_as_attr,
    Some(SerdeAsFieldAttr::CustomOverride { ref custom_type, .. }) if custom_type == "crate::MyDate"
  ));

  let tag_id_field = struct_def
    .fields
    .iter()
    .find(|f| f.name == "tag_id")
    .expect("tag_id field should exist");
  assert!(matches!(
    tag_id_field.serde_as_attr,
    Some(SerdeAsFieldAttr::CustomOverride { ref custom_type, .. }) if custom_type == "crate::MyUuid"
  ));

  Ok(())
}
