#[cfg(feature = "miette")]
use miette::Diagnostic;
use relative_path::RelativePathBuf;
use serde::Deserialize;
use serde_json::{Map, Value};
use thiserror::Error;

use crate::{action::Action, semver::Version};

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TauriConfJson {
    path: RelativePathBuf,
    raw: String,
    parsed: Json,
    diff: Option<String>,
}

impl TauriConfJson {
    pub(crate) fn new(path: RelativePathBuf, content: String) -> Result<Self, Error> {
        match serde_json::from_str(&content) {
            Ok(parsed) => Ok(TauriConfJson {
                path,
                raw: content,
                parsed,
                diff: None,
            }),
            Err(err) => Err(Error::Deserialize { path, source: err }),
        }
    }

    pub(crate) fn get_version(&self) -> &Version {
        &self.parsed.version
    }

    pub(crate) fn get_path(&self) -> &RelativePathBuf {
        &self.path
    }

    pub(crate) fn set_version(mut self, new_version: &Version) -> serde_json::Result<Self> {
        let mut json = serde_json::from_str::<Map<String, Value>>(&self.raw)?;
        json.insert(
            "version".to_string(),
            Value::String(new_version.to_string()),
        );
        self.raw = serde_json::to_string_pretty(&json)?;
        self.diff = Some(new_version.to_string());
        Ok(self)
    }

    pub(crate) fn write(self) -> Option<Action> {
        self.diff.map(|diff| Action::WriteToFile {
            path: self.path,
            content: self.raw,
            diff,
        })
    }
}

#[derive(Debug, Error)]
#[cfg_attr(feature = "miette", derive(Diagnostic))]
pub enum Error {
    #[error("Error deserializing {path}: {source}")]
    #[cfg_attr(
        feature = "miette",
        diagnostic(
            code(tauri_conf_json::deserialize),
            help(
                "knope expects the tauri.conf.json file to be an object with a top level `version` property"
            ),
            url("https://knope.tech/reference/config-file/packages/#tauri-conf")
        )
    )]
    Deserialize {
        path: RelativePathBuf,
        #[source]
        source: serde_json::Error,
    },
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
struct Json {
    version: Version,
}

#[cfg(test)]
mod tests {
    use std::str::FromStr;

    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_get_version() {
        let content = r#"{
        "productName": "tester",
        "version": "0.1.0-rc.0"
        }"#;

        assert_eq!(
            TauriConfJson::new(RelativePathBuf::new(), content.to_string())
                .unwrap()
                .get_version(),
            &Version::from_str("0.1.0-rc.0").unwrap()
        );
    }

    #[test]
    fn test_set_version() {
        let content = r#"{
        "productName": "tester",
        "version": "0.1.0-rc.0"
        }"#;

        let new = TauriConfJson::new(RelativePathBuf::new(), content.to_string())
            .unwrap()
            .set_version(&Version::from_str("1.2.3-rc.4").unwrap())
            .unwrap()
            .write()
            .expect("diff to write");

        let expected = r#"{
  "productName": "tester",
  "version": "1.2.3-rc.4"
}"#
        .to_string();
        let expected = Action::WriteToFile {
            path: RelativePathBuf::new(),
            content: expected,
            diff: "1.2.3-rc.4".to_string(),
        };
        assert_eq!(new, expected);
    }

    #[test]
    fn retain_property_order() {
        let content = r#"{
        "productName": "tester",
        "version": "0.1.0-rc.0",
        "identifier": "com.knope.tester"
        }"#;

        let new = TauriConfJson::new(RelativePathBuf::new(), content.to_string())
            .unwrap()
            .set_version(&Version::from_str("1.2.3-rc.4").unwrap())
            .unwrap()
            .write()
            .expect("diff to write");

        let expected = r#"{
  "productName": "tester",
  "version": "1.2.3-rc.4",
  "identifier": "com.knope.tester"
}"#
        .to_string();
        let expected = Action::WriteToFile {
            path: RelativePathBuf::new(),
            content: expected,
            diff: "1.2.3-rc.4".to_string(),
        };
        assert_eq!(new, expected);
    }
}
