use cairo_lang_debug::DebugWithDb;
use cairo_lang_defs::ids::{
    EnumId, ImplDefId, ImplItemId, LookupItemId, ModuleId, ModuleItemId, StructId, TraitId,
    TraitItemId,
};
use cairo_lang_semantic::items::enm::EnumSemantic;
use cairo_lang_semantic::items::imp::ImplSemantic;
use cairo_lang_semantic::items::structure::StructSemantic;
use cairo_lang_semantic::items::trt::TraitSemantic;
use cairo_lang_test_utils::parse_test_file::TestRunnerResult;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
use itertools::Itertools;

use super::test_utils::{TestDatabase, set_file_content, setup_test_module};
use crate::db::DocGroup;
use crate::documentable_item::DocumentableItemId;
use crate::parser::DocumentationCommentToken;
use crate::tests::test_utils::test_crate_id;

cairo_lang_test_utils::test_file_test!(
  item_documentation,
  "src/tests/test-data",
  {
    basic: "basic.txt",
    submodule: "submodule.txt",
    trivia: "trivia.txt",
    comment_markers: "comment_markers.txt",
    signature: "signature.txt",
    lists_formatting: "lists_formatting.txt",
    tables_formatting: "tables_formatting.txt",
    rules_formatting: "rules_formatting.txt",
    font_formatting: "font_formatting.txt",
  },
  documentation_test_runner
);

fn documentation_test_runner(
    inputs: &OrderedHashMap<String, String>,
    _args: &OrderedHashMap<String, String>,
) -> TestRunnerResult {
    let mut db_val = TestDatabase::new().unwrap();
    setup_test_module(&mut db_val, inputs["cairo_code"].as_str());
    let submodule_code = inputs.get("cairo_submodule_code");

    if let Some(submodule_code) = submodule_code {
        set_file_content(&mut db_val, "src/cairo_submodule_code.cairo", submodule_code);
    }

    let db = &db_val;

    let mut result_doc_builder = ResultDocBuilder::new(db);
    let crate_id = test_crate_id(db);
    result_doc_builder.document_module(ModuleId::CrateRoot(crate_id));

    TestRunnerResult::success(result_doc_builder.get_output())
}

struct ResultDocBuilder<'a> {
    db: &'a TestDatabase,
    item_counter: u32,
    output: OrderedHashMap<String, String>,
}

impl<'a> ResultDocBuilder<'a> {
    fn new(db: &'a TestDatabase) -> Self {
        Self { db, item_counter: 1, output: OrderedHashMap::default() }
    }

    fn get_output(self) -> OrderedHashMap<String, String> {
        self.output
    }

    fn document_module(&mut self, module_id: ModuleId<'_>) {
        let module_doc = match module_id {
            ModuleId::CrateRoot(crate_id) => {
                self.db.get_item_documentation(DocumentableItemId::Crate(crate_id))
            }
            ModuleId::Submodule(submodule_id) => {
                self.db.get_item_documentation(DocumentableItemId::from(LookupItemId::ModuleItem(
                    ModuleItemId::Submodule(submodule_id),
                )))
            }
            ModuleId::MacroCall { .. } => None,
        };

        let doc_tokens = match module_id {
            ModuleId::CrateRoot(crate_id) => {
                self.db.get_item_documentation_as_tokens(DocumentableItemId::Crate(crate_id))
            }
            ModuleId::Submodule(submodule_id) => {
                self.db.get_item_documentation_as_tokens(DocumentableItemId::from(
                    LookupItemId::ModuleItem(ModuleItemId::Submodule(submodule_id)),
                ))
            }
            ModuleId::MacroCall { .. } => None,
        };

        self.insert_doc_to_test_output(module_doc, Some("".to_owned()), doc_tokens);

        let submodule_items = module_id.module_data(self.db).unwrap().items(self.db);

        for submodule_item_id in submodule_items.iter() {
            match submodule_item_id {
                ModuleItemId::Struct(struct_id) => {
                    self.document_struct_with_members(struct_id);
                }
                ModuleItemId::Enum(enum_id) => {
                    self.document_enum_with_variants(enum_id);
                }
                ModuleItemId::Trait(trait_id) => {
                    self.document_trait_with_items(trait_id);
                }
                ModuleItemId::Impl(impl_id) => {
                    self.document_impl_with_items(impl_id);
                }
                ModuleItemId::Submodule(module_id) => {
                    self.document_module(ModuleId::Submodule(*module_id))
                }
                _ => {
                    let id = DocumentableItemId::from(LookupItemId::ModuleItem(*submodule_item_id));
                    let item_doc = self.db.get_item_documentation(id);
                    let item_signature = self.db.get_item_signature(id);
                    let item_doc_tokens = self.db.get_item_documentation_as_tokens(id);
                    self.insert_doc_to_test_output(item_doc, item_signature, item_doc_tokens);
                }
            }
        }
    }

    fn document_struct_with_members(&mut self, struct_id: &StructId<'_>) {
        let id =
            DocumentableItemId::from(LookupItemId::ModuleItem(ModuleItemId::Struct(*struct_id)));
        let struct_doc = self.db.get_item_documentation(id);
        let struct_signature = self.db.get_item_signature(id);
        let struct_doc_tokens = self.db.get_item_documentation_as_tokens(id);
        self.insert_doc_to_test_output(struct_doc, struct_signature, struct_doc_tokens);
        let members = self.db.struct_members(*struct_id).unwrap();

        members.iter().for_each(|(_, semantic_member)| {
            let id = DocumentableItemId::from(semantic_member.id);
            let member_doc = self.db.get_item_documentation(id);
            let member_signature = self.db.get_item_signature(id);
            let member_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(member_doc, member_signature, member_doc_tokens);
        });
    }

    fn document_enum_with_variants(&mut self, enum_id: &EnumId<'_>) {
        let id = DocumentableItemId::from(LookupItemId::ModuleItem(ModuleItemId::Enum(*enum_id)));
        let enum_doc = self.db.get_item_documentation(id);
        let enum_signature = self.db.get_item_signature(id);
        let enum_doc_tokens = self.db.get_item_documentation_as_tokens(id);
        self.insert_doc_to_test_output(enum_doc, enum_signature, enum_doc_tokens);
        let variants = self.db.enum_variants(*enum_id).unwrap();

        variants.iter().for_each(|(_, variant_id)| {
            let id = DocumentableItemId::Variant(*variant_id);
            let variant_doc = self.db.get_item_documentation(id);
            let variant_signature = self.db.get_item_signature(id);
            let variant_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(variant_doc, variant_signature, variant_doc_tokens);
        })
    }

    fn document_trait_with_items(&mut self, trait_id: &TraitId<'_>) {
        let id = DocumentableItemId::from(LookupItemId::ModuleItem(ModuleItemId::Trait(*trait_id)));
        let trait_doc = self.db.get_item_documentation(id);
        let trait_signature = self.db.get_item_signature(id);
        let trait_doc_tokens = self.db.get_item_documentation_as_tokens(id);
        self.insert_doc_to_test_output(trait_doc, trait_signature, trait_doc_tokens);
        let trait_constants = self.db.trait_constants(*trait_id).unwrap();
        let trait_types = self.db.trait_types(*trait_id).unwrap();
        let trait_functions = self.db.trait_functions(*trait_id).unwrap();

        trait_constants.iter().for_each(|(_, trait_constant_id)| {
            let id = DocumentableItemId::from(LookupItemId::TraitItem(TraitItemId::Constant(
                *trait_constant_id,
            )));
            let trait_constant_doc = self.db.get_item_documentation(id);
            let trait_constant_signature = self.db.get_item_signature(id);
            let trait_constant_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                trait_constant_doc,
                trait_constant_signature,
                trait_constant_doc_tokens,
            );
        });

        trait_types.iter().for_each(|(_, trait_type_id)| {
            let id = DocumentableItemId::from(LookupItemId::TraitItem(TraitItemId::Type(
                *trait_type_id,
            )));
            let trait_type_doc = self.db.get_item_documentation(id);
            let trait_type_signature = self.db.get_item_signature(id);
            let trait_type_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                trait_type_doc,
                trait_type_signature,
                trait_type_doc_tokens,
            );
        });

        trait_functions.iter().for_each(|(_, trait_function_id)| {
            let id = DocumentableItemId::from(LookupItemId::TraitItem(TraitItemId::Function(
                *trait_function_id,
            )));
            let trait_function_doc = self.db.get_item_documentation(id);
            let trait_function_signature = self.db.get_item_signature(id);
            let trait_function_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                trait_function_doc,
                trait_function_signature,
                trait_function_doc_tokens,
            );
        });
    }

    fn document_impl_with_items(&mut self, impl_id: &ImplDefId<'_>) {
        let id = DocumentableItemId::from(LookupItemId::ModuleItem(ModuleItemId::Impl(*impl_id)));
        let impl_doc = self.db.get_item_documentation(id);
        let impl_signature = self.db.get_item_signature(id);
        let impl_doc_tokens = self.db.get_item_documentation_as_tokens(id);
        self.insert_doc_to_test_output(impl_doc, impl_signature, impl_doc_tokens);
        let impl_types = self.db.impl_types(*impl_id).unwrap();
        let impl_constants = self.db.impl_constants(*impl_id).unwrap();
        let impl_functions = self.db.impl_functions(*impl_id).unwrap();

        impl_types.iter().for_each(|(impl_type_id, _)| {
            let id =
                DocumentableItemId::from(LookupItemId::ImplItem(ImplItemId::Type(*impl_type_id)));
            let impl_type_doc = self.db.get_item_documentation(id);
            let impl_type_signature = self.db.get_item_signature(id);
            let impl_type_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                impl_type_doc,
                impl_type_signature,
                impl_type_doc_tokens,
            );
        });

        impl_constants.iter().for_each(|(impl_constant_id, _)| {
            let id = DocumentableItemId::from(LookupItemId::ImplItem(ImplItemId::Constant(
                *impl_constant_id,
            )));
            let impl_constant_doc = self.db.get_item_documentation(id);
            let impl_constant_signature = self.db.get_item_signature(id);
            let impl_constant_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                impl_constant_doc,
                impl_constant_signature,
                impl_constant_doc_tokens,
            );
        });

        impl_functions.iter().for_each(|(_, impl_function_id)| {
            let id = DocumentableItemId::from(LookupItemId::ImplItem(ImplItemId::Function(
                *impl_function_id,
            )));
            let impl_function_doc = self.db.get_item_documentation(id);
            let impl_function_signature = self.db.get_item_signature(id);
            let impl_function_doc_tokens = self.db.get_item_documentation_as_tokens(id);
            self.insert_doc_to_test_output(
                impl_function_doc,
                impl_function_signature,
                impl_function_doc_tokens,
            );
        });
    }

    fn insert_doc_to_test_output(
        &mut self,
        documentation: Option<String>,
        signature: Option<String>,
        documentation_as_tokens: Option<Vec<DocumentationCommentToken<'_>>>,
    ) {
        if let Some(signature) = signature {
            self.output
                .insert("Item signature #".to_string() + &self.item_counter.to_string(), signature);
        }
        self.output.insert(
            "Item documentation #".to_string() + &self.item_counter.to_string(),
            documentation.unwrap_or_default(),
        );
        self.output.insert(
            "Item documentation tokens #".to_string() + &self.item_counter.to_string(),
            documentation_as_tokens
                .unwrap_or_default()
                .iter()
                .map(|token| match token {
                    DocumentationCommentToken::Content(_) => format!("{token:?}"),
                    DocumentationCommentToken::Link(link_token) => {
                        format!("{:?}", link_token.debug(self.db))
                    }
                })
                .join("\n"),
        );
        self.item_counter += 1;
    }
}
