use crate::config::TransformConfig;
#[double]
use crate::file::{FileReader, FileWriter};
use crate::version::update::VersionUpdate;
use crate::{token, SheepError};
use mockall_double::double;
use std::path::{Path, PathBuf};

pub struct FileTransformer<'a> {
    config: &'a TransformConfig,
    file_reader: &'a FileReader,
    file_writer: &'a FileWriter,
    project_path: &'a Path,
}

impl<'a> FileTransformer<'a> {
    pub fn new(
        config: &'a TransformConfig,
        file_reader: &'a FileReader,
        file_writer: &'a FileWriter,
        project_path: &'a Path,
    ) -> Self {
        Self {
            config,
            file_reader,
            file_writer,
            project_path,
        }
    }

    pub fn transform(&self, version_update: &VersionUpdate) -> Result<String, SheepError> {
        let relative_path = &self.config.path;
        let path = self.full_path(relative_path);
        let file_text = self.file_reader.read_to_string(&path)?;
        let transformed = file_text.replacen(
            &self.find_string(version_update),
            &self.replace_string(version_update),
            1,
        );
        self.file_writer.write_string_to_file(&path, &transformed)?;
        Ok(relative_path.clone())
    }

    fn full_path(&self, relative_path: &str) -> PathBuf {
        [self.project_path, &Path::new(relative_path)]
            .iter()
            .collect()
    }

    fn find_string(&self, version_update: &VersionUpdate) -> String {
        let find = self
            .config
            .find
            .clone()
            .unwrap_or(self.config.replace.clone());
        find.replacen(
            token::VERSION,
            &version_update.current_version.to_string(),
            1,
        )
    }

    fn replace_string(&self, version_update: &VersionUpdate) -> String {
        let replace = self.config.replace.clone();
        replace.replacen(token::VERSION, &version_update.next_version.to_string(), 1)
    }
}

#[cfg(test)]
mod test {
    use crate::config::TransformConfig;
    use crate::file::{MockFileReader, MockFileWriter};
    use crate::transform::file_transform::FileTransformer;
    use crate::version::update::VersionUpdate;
    use semver::Version;
    use std::path::PathBuf;

    const PATH: &str = "path";
    const PROJECT_PATH: &str = "project";
    const FULL_PATH: &str = "project/path";

    #[test]
    fn transform_find_and_replace_no_version() {
        let reader = mock_reader("find");
        let writer = mock_writer("replace");
        let config = TransformConfig {
            path: PATH.to_string(),
            find: Some("find".to_string()),
            replace: "replace".to_string(),
        };
        let project_path = project_path();
        let file_transformer = FileTransformer::new(&config, &reader, &writer, &project_path);
        let path = file_transformer
            .transform(&version_update())
            .expect("transform failed");
        assert_eq!(PATH, path)
    }

    #[test]
    fn transform_find_and_replace_find_version() {
        let reader = mock_reader("find_1.0.0");
        let writer = mock_writer("replace");
        let config = TransformConfig {
            path: PATH.to_string(),
            find: Some("find_{version}".to_string()),
            replace: "replace".to_string(),
        };
        let project_path = project_path();
        let file_transformer = FileTransformer::new(&config, &reader, &writer, &project_path);
        let path = file_transformer
            .transform(&version_update())
            .expect("transform failed");
        assert_eq!(PATH, path)
    }

    #[test]
    fn transform_find_and_replace_both_version() {
        let reader = mock_reader("find_1.0.0");
        let writer = mock_writer("replace__2.0.0");
        let config = TransformConfig {
            path: PATH.to_string(),
            find: Some("find_{version}".to_string()),
            replace: "replace__{version}".to_string(),
        };
        let project_path = project_path();
        let file_transformer = FileTransformer::new(&config, &reader, &writer, &project_path);
        let path = file_transformer
            .transform(&version_update())
            .expect("transform failed");
        assert_eq!(PATH, path)
    }

    #[test]
    fn transform_only_replace() {
        let reader = mock_reader("version_1.0.0");
        let writer = mock_writer("version_2.0.0");
        let config = TransformConfig {
            path: PATH.to_string(),
            find: None,
            replace: "version_{version}".to_string(),
        };
        let project_path = project_path();
        let file_transformer = FileTransformer::new(&config, &reader, &writer, &project_path);
        let path = file_transformer
            .transform(&version_update())
            .expect("transform failed");
        assert_eq!(PATH, path)
    }

    fn project_path() -> PathBuf {
        PathBuf::from(PROJECT_PATH)
    }

    fn version_update() -> VersionUpdate {
        VersionUpdate {
            current_version: Version::parse("1.0.0").expect("failed to parse version"),
            next_version: Version::parse("2.0.0").expect("failed to parse version"),
        }
    }

    fn mock_reader(text: &str) -> MockFileReader {
        let text_copy = text.to_string();
        let mut mock = MockFileReader::default();
        mock.expect_read_to_string()
            .withf_st(|p| p.as_ref().to_str().unwrap() == FULL_PATH)
            .return_once(|_| Ok(text_copy));
        mock
    }

    fn mock_writer(expected: &str) -> MockFileWriter {
        let expected_copy = expected.to_string();
        let mut mock = MockFileWriter::default();
        mock.expect_write_string_to_file()
            .withf_st(move |p, t| {
                p.as_ref().to_str().unwrap() == FULL_PATH && t == expected_copy.clone()
            })
            .return_once(|_, _| Ok(()));
        mock
    }
}
