use std::env::current_dir;
use std::path::PathBuf;
use wtools::error::BasicError;
use wca::instruction::Instruction;
use ::wpublisher::manifest::Manifest;

///
/// Perform smoke testing.
///

pub fn smoke( instruction : &Instruction ) -> Result< (), BasicError >
{
  let mut current_path = current_dir().unwrap();

  let subject_path = PathBuf::from( &instruction.subject );
  let module_path = if subject_path.is_relative()
  {
    current_path.push( &instruction.subject );
    current_path
  }
  else
  {
    subject_path
  };

  let mut manifest_path = module_path.clone();
  manifest_path.push( "Cargo.toml" );

  if !manifest_path.exists()
  {
    let msg = format!( "Current directory {:?} has no file \"Cargo.toml\"", module_path.canonicalize().unwrap() );
    return Err( BasicError::new( msg ) );
  }

  let mut manifest = Manifest::new();
  manifest.manifest_path_from_str( &manifest_path ).unwrap();
  manifest.load().unwrap();
  let data = manifest.manifest_data.as_deref().unwrap();

  /* */

  let module_name = &data[ "package" ][ "name" ].clone();
  let module_name = module_name.as_str().unwrap();

  let code_path = match instruction.properties_map.get( "code_path" )
  {
    Some( x ) => PathBuf::from( x.clone().primitive().unwrap() ),
    None => PathBuf::default(),
  };

  let mut data = None;
  if code_path.exists()
  {
    data = Some( std::fs::read_to_string( code_path ).unwrap() );
  }

  let version = match instruction.properties_map.get( "version" )
  {
    Some( x ) => x.clone().primitive().unwrap(),
    None => "*".to_string(),
  };

  let smoke = match instruction.properties_map.get( "smoke" )
  {
    Some( x ) => x.clone().primitive().unwrap(),
    None =>
    {
      if let Ok( x ) = std::env::var( "WITH_SMOKE" )
      {
        x
      }
      else
      {
        "local".to_string()
      }
    },
  };

  /* */

  if smoke != "false" && smoke != "0"
  {
    let mut threads = vec![];
    if smoke == "local" || smoke != "published"
    {
      let module_name = module_name.clone().to_owned();
      let data = data.clone();
      let version = version.clone();
      let thread = std::thread::spawn( move ||
      {
        let mut t = SmokeModuleTest::new( module_name );
        t.test_postfix( "_test_local" );
        if data.is_some()
        {
          t.code( data.as_ref().unwrap() );
        }
        t.version( version.as_str() );
        t.local_path_clause( module_path.to_str().unwrap() );

        t.clean( true ).unwrap();
        t.form().unwrap();
        t.perform().unwrap();
        t.clean( false ).unwrap();
      });
      threads.push( thread );
    }

    if smoke == "published" || smoke != "local"
    {
      let module_name = module_name.clone().to_owned();
      let data = data.clone();
      let version = version.clone();
      let thread = std::thread::spawn( move ||
      {
        let mut t = SmokeModuleTest::new( module_name );
        t.test_postfix( "_test_published" );
        if data.is_some()
        {
          t.code( data.as_ref().unwrap() );
        }
        t.version( version.as_str() );

        t.clean( true ).unwrap();
        t.form().unwrap();
        t.perform().unwrap();
        t.clean( false ).unwrap();
      });
      threads.push( thread );
    }

    for thread in threads
    {
      thread.join().unwrap();
    }
  }

  Ok( () )
}

//

#[ derive( Debug ) ]
struct SmokeModuleTest< 'a >
{
  pub dependency_name : String,
  pub version : &'a str,
  pub local_path_clause : &'a str,
  pub code : String,
  pub test_path : std::path::PathBuf,
  pub test_postfix : &'a str,
}

impl< 'a > SmokeModuleTest< 'a >
{
  fn new( dependency_name : String ) -> SmokeModuleTest< 'a >
  {
    let test_postfix = "_smoke_test";
    let smoke_test_path = format!( "{}{}", dependency_name, test_postfix );
    let mut test_path = std::env::temp_dir();
    test_path.push( smoke_test_path );

    SmokeModuleTest
    {
      dependency_name,
      version : "*",
      local_path_clause : "",
      code : "".to_string(),
      test_path,
      test_postfix,
    }
  }

  fn version( &mut self, version : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.version = version.as_ref();
    self
  }

  fn local_path_clause( &mut self, local_path_clause : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.local_path_clause = local_path_clause;
    self
  }

  fn test_postfix( &mut self, test_postfix : &'a str ) -> &mut SmokeModuleTest< 'a >
  {
    self.test_postfix = test_postfix;
    let smoke_test_path = format!( "{}{}", self.dependency_name, test_postfix );
    self.test_path.pop();
    self.test_path.push( smoke_test_path );
    self
  }

  fn code( &mut self, code : impl AsRef< str > + 'a ) -> &mut SmokeModuleTest< 'a >
  {
    self.code = code.as_ref().into();
    self
  }

  fn form( &mut self ) -> Result< (), &'static str >
  {
    std::fs::create_dir( &self.test_path ).unwrap();

    let mut test_path = self.test_path.clone();

    /* create binary test module */
    let test_name = format!( "{}{}", self.dependency_name, self.test_postfix );
    let output = std::process::Command::new( "cargo" )
    .current_dir( &test_path )
    .args([ "new", "--bin", &test_name ])
    .output()
    .expect( "Failed to execute command" );
    println!( "Creating smoke binary module :\n\n{}", std::str::from_utf8( &output.stderr ).expect( "Found invalid UTF-8" ) );

    test_path.push( &test_name );

    /* setup config */
    #[ cfg( target_os = "windows" ) ]
    let local_path_clause = if self.local_path_clause == "" { "".to_string() } else { format!( ", path = \"{}\"", self.local_path_clause.escape_default() ) };
    #[ cfg( not( target_os = "windows" ) ) ]
    let local_path_clause = if self.local_path_clause == "" { "".to_string() } else { format!( ", path = \"{}\"", self.local_path_clause ) };
    let dependencies_section = format!( "{} = {{ version = \"{}\" {} }}", self.dependency_name, self.version, &local_path_clause );
    let config_data = format!
    (
      "[package]
      edition = \"2021\"
      name = \"{}_smoke_test\"
      version = \"0.0.1\"

      [dependencies]
      {}",
      &self.dependency_name,
      &dependencies_section
    );
    let mut config_path = test_path.clone();
    config_path.push( "Cargo.toml" );
    println!( "Manifest of module \"{}\" :\n\n      {}\n", test_name, config_data );
    std::fs::write( config_path, config_data ).unwrap();

    /* write code */
    test_path.push( "src" );
    test_path.push( "main.rs" );
    if self.code == ""
    {
      self.code = format!( "use ::{}::*;", self.dependency_name );
    }
    let code = format!
    (
      "#[ allow( unused_imports ) ]
      fn main()
      {{
        {}
      }}",
      self.code,
    );
    self.code = code;
    std::fs::write( &test_path, &self.code ).unwrap();

    Ok( () )
  }

  fn perform( &self ) -> Result<(), BasicError>
  {
    let mut test_path = self.test_path.clone();
    let test_name = format!( "{}{}", self.dependency_name, self.test_postfix );
    test_path.push( test_name );

    let output = std::process::Command::new( "cargo" )
    .current_dir( test_path )
    .args([ "run", "--release" ])
    .output()
    .unwrap();
    println!( "{}", std::str::from_utf8( &output.stdout ).expect( "Found invalid UTF-8" ) );
    println!( "{}", std::str::from_utf8( &output.stderr ).expect( "Found invalid UTF-8" ) );
    println!( "Process status :\n  {}\n", output.status );
    println!( "Code :\n\n      {}\n", self.code );

    if !output.status.success()
    {
      return Err( BasicError::new( "Smoke test failed" ) );
    }

    Ok( () )
  }

  fn clean( &self, force : bool ) -> Result<(), &'static str>
  {
    let result = std::fs::remove_dir_all( &self.test_path );
    if force
    {
      result.unwrap_or_default();
    }
    else
    {
      let msg = format!( "Cannot remove temporary directory {}. Please, remove it manually", &self.test_path.display() );
      result.expect( &msg );
    }
    Ok( () )
  }
}

