use std::path::Path;

use crate::languages::{Language, LanguageConfig};
use crate::library::BoxedExtractor;
use daipendency_extractor::{LibraryMetadata, LibraryMetadataError};
use thiserror::Error;

pub fn get_extractor(language: Language) -> BoxedExtractor {
    let config = LanguageConfig::get_from_language(language);

    (config.extractor_initialiser)()
}

pub struct ExtractorDiscovery {
    pub language: Language,
    pub extractor: BoxedExtractor,
    pub library_metadata: LibraryMetadata,
}

impl std::fmt::Debug for ExtractorDiscovery {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ExtractorDiscovery")
            .field("language", &self.language)
            .field("extractor", &"<dyn Extractor>")
            .field("library_metadata", &self.library_metadata)
            .finish()
    }
}

#[derive(Error, Debug)]
pub enum ExtractorDiscoveryError {
    #[error("Malformed manifest for {language:?}: {error}")]
    MalformedManifest { language: Language, error: String },
    #[error("No matching extractor found")]
    NotFound,
}

pub fn discover_extractor(path: &Path) -> Result<ExtractorDiscovery, ExtractorDiscoveryError> {
    for language in LanguageConfig::get_all().keys() {
        let extractor = get_extractor(*language);
        match extractor.get_library_metadata(path) {
            Ok(library_metadata) => {
                return Ok(ExtractorDiscovery {
                    language: *language,
                    extractor,
                    library_metadata,
                })
            }
            Err(LibraryMetadataError::MalformedManifest(error)) => {
                return Err(ExtractorDiscoveryError::MalformedManifest {
                    language: *language,
                    error,
                })
            }
            Err(LibraryMetadataError::MissingManifest(_)) => continue,
        }
    }
    Err(ExtractorDiscoveryError::NotFound)
}

#[cfg(test)]
mod tests {
    use super::*;

    mod extractor_getter {
        use super::*;
        use daipendency_extractor::Extractor;
        use daipendency_extractor_rust::RustExtractor;

        #[test]
        fn get_extractor_rust() {
            let extractor = get_extractor(Language::Rust);

            assert_eq!(
                format!("{:?}", extractor.get_parser_language()),
                format!("{:?}", RustExtractor::new().get_parser_language())
            );
        }
    }

    mod extractor_discovery {
        use super::*;
        use assertables::{assert_matches, assert_ok};
        use daipendency_extractor::Extractor;
        use daipendency_extractor_rust::RustExtractor;
        use std::fs;
        use tempfile::TempDir;

        #[test]
        fn no_matching_extractor() {
            let temp_dir = TempDir::new().unwrap();

            let result = discover_extractor(temp_dir.path());

            assert_matches!(result, Err(ExtractorDiscoveryError::NotFound));
        }

        #[test]
        fn malformed_manifest() {
            let temp_dir = TempDir::new().unwrap();
            fs::write(temp_dir.path().join("Cargo.toml"), "invalid toml").unwrap();

            let result = discover_extractor(temp_dir.path());

            assert_matches!(
                result,
                Err(ExtractorDiscoveryError::MalformedManifest {
                    language: Language::Rust,
                    error: _
                })
            );
        }

        #[test]
        fn successful_discovery() {
            let temp_dir = TempDir::new().unwrap();
            fs::write(
                temp_dir.path().join("Cargo.toml"),
                r#"[package]
name = "test-package"
version = "0.1.0"
"#,
            )
            .unwrap();

            let result = discover_extractor(temp_dir.path());

            assert_ok!(&result);
            let discovery = result.unwrap();
            assert_eq!(discovery.language, Language::Rust);
            assert_eq!(discovery.library_metadata.name, "test-package");
            assert_eq!(
                format!("{:?}", discovery.extractor.get_parser_language()),
                format!("{:?}", RustExtractor::new().get_parser_language())
            );
        }
    }
}
