// Copyright 2019-2025 Parity Technologies (UK) Ltd.
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
// see LICENSE for license details.

use codec::{Decode, Encode};
use frame_metadata::RuntimeMetadataPrefixed;
use std::io::Read;

static TEST_DIR_PREFIX: &str = "subxt_generated_ui_tests_";
static METADATA_FILE: &str = "../../artifacts/polkadot_metadata_full.scale";

#[derive(Default)]
pub struct MetadataTestRunner {
    index: usize,
}

impl MetadataTestRunner {
    /// Loads metadata that we can use in our tests. Panics if
    /// there is some issue decoding the metadata.
    pub fn load_metadata() -> RuntimeMetadataPrefixed {
        let mut file =
            std::fs::File::open(METADATA_FILE).expect("Cannot open metadata.scale artifact");

        let mut bytes = Vec::new();
        file.read_to_end(&mut bytes)
            .expect("Failed to read metadata.scale file");

        RuntimeMetadataPrefixed::decode(&mut &*bytes).expect("Cannot decode metadata bytes")
    }

    /// Create a new test case.
    pub fn new_test_case(&mut self) -> MetadataTestRunnerCaseBuilder {
        let index = self.index;
        // increment index so that each test case gets its own folder path.
        self.index += 1;

        MetadataTestRunnerCaseBuilder::new(index)
    }
}

// `trybuild` runs all tests once it's dropped. So, we defer all cleanup until we
// are dropped too, to make sure that cleanup happens after tests are ran.
impl Drop for MetadataTestRunner {
    fn drop(&mut self) {
        for i in 0..self.index {
            let mut tmp_dir = std::env::temp_dir();
            tmp_dir.push(format!("{TEST_DIR_PREFIX}{i}"));
            std::fs::remove_dir_all(tmp_dir).expect("cannot cleanup temp files");
        }
    }
}

/// Build a single test case.
pub struct MetadataTestRunnerCaseBuilder {
    index: usize,
    name: String,
    validation_metadata: Option<RuntimeMetadataPrefixed>,
    should_be_valid: bool,
}

impl MetadataTestRunnerCaseBuilder {
    fn new(index: usize) -> Self {
        MetadataTestRunnerCaseBuilder {
            index,
            name: format!("Test {index}"),
            validation_metadata: None,
            should_be_valid: true,
        }
    }

    /// Set the test name.
    pub fn name(mut self, name: impl AsRef<str>) -> Self {
        name.as_ref().clone_into(&mut self.name);
        self
    }

    /// Set metadata to be validated against the generated code.
    /// By default, we'll validate the same metadata used to generate the code.
    pub fn validation_metadata(mut self, md: impl Into<RuntimeMetadataPrefixed>) -> Self {
        self.validation_metadata = Some(md.into());
        self
    }

    /// Expect the validation metadata provided to _not_ be valid.
    pub fn expects_invalid(mut self) -> Self {
        self.should_be_valid = false;
        self
    }

    /// At the minimum, takes some metadata and a test name, generates the code
    /// and hands back a path to some generated code that `trybuild` can be pointed at.
    /// validation metadata and expected validity can also be provided.
    ///
    /// The generated code:
    /// - checks that the subxt macro can perform codegen given the
    ///   provided macro_metadata without running into any issues.
    /// - checks that the `runtime::is_codegen_valid_for` function returns
    ///   true or false when compared to the `validation_metadata`, according
    ///   to whether `expects_invalid()` is set or not.
    ///
    /// The generated code will be tidied up when the `MetadataTestRunner` that
    /// this was handed out from is dropped.
    pub fn build(self, macro_metadata: frame_metadata::RuntimeMetadataPrefixed) -> String {
        let validation_metadata = self.validation_metadata.unwrap_or_else(|| {
            // RuntimeMetadataPrefixed doesn't implement Clone for some reason (we should prob fix that).
            // until then, this hack clones it by encoding and then decoding it again from bytes..
            clone_via_encode(&macro_metadata)
        });

        let index = self.index;
        let mut tmp_dir = std::env::temp_dir();
        tmp_dir.push(format!("{TEST_DIR_PREFIX}{index}"));

        let tmp_macro_metadata_path = {
            let mut t = tmp_dir.clone();
            t.push("macro_metadata.scale");
            t.to_string_lossy().into_owned()
        };
        let tmp_validation_metadata_path = {
            let mut t = tmp_dir.clone();
            t.push("validation_metadata.scale");
            t.to_string_lossy().into_owned()
        };
        let tmp_rust_path = {
            let mut t = tmp_dir.clone();
            let test_name = &self.name;
            t.push(format!("{test_name}.rs"));
            t.to_string_lossy().into_owned()
        };

        let encoded_macro_metadata = macro_metadata.encode();
        let encoded_validation_metadata = validation_metadata.encode();

        let should_be_valid_str = if self.should_be_valid {
            "true"
        } else {
            "false"
        };

        let rust_file = format!(
            r#"
            use subxt;
            use subxt::ext::codec::Decode;
            use std::io::Read;

            #[subxt::subxt(runtime_metadata_path = "{tmp_macro_metadata_path}")]
            pub mod polkadot {{}}

            fn main() {{
                // load validation metadata:
                let mut file = std::fs::File::open("{tmp_validation_metadata_path}")
                    .expect("validation_metadata exists");

                let mut bytes = Vec::new();
                file.read_to_end(&mut bytes)
                    .expect("Failed to read metadata.scale file");

                let metadata = subxt::Metadata::decode(&mut &*bytes)
                    .expect("Cannot decode metadata bytes");

                // validate it:
                let is_valid = polkadot::is_codegen_valid_for(&metadata);
                assert_eq!(is_valid, {should_be_valid_str}, "expected validity to line up");
            }}
        "#
        );

        std::fs::create_dir_all(&tmp_dir).expect("could not create tmp ui test dir");
        // Write metadatas to tmp folder:
        std::fs::write(&tmp_macro_metadata_path, encoded_macro_metadata).unwrap();
        std::fs::write(&tmp_validation_metadata_path, encoded_validation_metadata).unwrap();
        // Write test file to tmp folder (it'll be moved by trybuild):
        std::fs::write(&tmp_rust_path, rust_file).unwrap();

        tmp_rust_path
    }
}

fn clone_via_encode<T: codec::Encode + codec::Decode>(item: &T) -> T {
    let bytes = item.encode();
    T::decode(&mut &*bytes).unwrap()
}
