use assert_cmd::prelude::*;
use assert_fs::fixture::FixtureError;
use assert_fs::prelude::*;
use indoc::indoc;
use lightningcss::css_modules::CssModuleExport;
use predicates::prelude::*;
use std::collections::HashMap;
use std::fs;
use std::process::Command;

fn test_file() -> Result<assert_fs::NamedTempFile, FixtureError> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str(
    r#"
      .foo {
        border: none;
      }
    "#,
  )?;
  Ok(file)
}

fn test_file2() -> Result<assert_fs::NamedTempFile, FixtureError> {
  let file = assert_fs::NamedTempFile::new("test2.css")?;
  file.write_str(
    r#"
      .foo {
        color: yellow;
      }
    "#,
  )?;
  Ok(file)
}

fn css_module_test_vals() -> (String, String, String) {
  let exports: HashMap<&str, CssModuleExport> = HashMap::from([
    (
      "fade",
      CssModuleExport {
        name: "EgL3uq_fade".into(),
        composes: vec![],
        is_referenced: false,
      },
    ),
    (
      "foo",
      CssModuleExport {
        name: "EgL3uq_foo".into(),
        composes: vec![],
        is_referenced: false,
      },
    ),
    (
      "circles",
      CssModuleExport {
        name: "EgL3uq_circles".into(),
        composes: vec![],
        is_referenced: true,
      },
    ),
    (
      "id",
      CssModuleExport {
        name: "EgL3uq_id".into(),
        composes: vec![],
        is_referenced: false,
      },
    ),
    (
      "test",
      CssModuleExport {
        name: "EgL3uq_test".into(),
        composes: vec![],
        is_referenced: true,
      },
    ),
  ]);
  (
    r#"
      .foo {
        color: red;
      }

      #id {
        animation: 2s test;
      }

      @keyframes test {
        from { color: red }
        to { color: yellow }
      }

      @counter-style circles {
        symbols: Ⓐ Ⓑ Ⓒ;
      }

      ul {
        list-style: circles;
      }

      @keyframes fade {
        from { opacity: 0 }
        to { opacity: 1 }
      }
    "#
    .into(),
    indoc! {r#"
      .EgL3uq_foo {
        color: red;
      }

      #EgL3uq_id {
        animation: 2s EgL3uq_test;
      }

      @keyframes EgL3uq_test {
        from {
          color: red;
        }

        to {
          color: #ff0;
        }
      }

      @counter-style EgL3uq_circles {
        symbols: Ⓐ Ⓑ Ⓒ;
      }

      ul {
        list-style: EgL3uq_circles;
      }

      @keyframes EgL3uq_fade {
        from {
          opacity: 0;
        }

        to {
          opacity: 1;
        }
      }
    "#}
    .into(),
    serde_json::to_string(&exports).unwrap(),
  )
}

#[test]
fn valid_input_file() -> Result<(), Box<dyn std::error::Error>> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str(
    r#"
      .foo {
        border: none;
      }
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
        .foo {
          border: none;
        }"#}));

  Ok(())
}

#[test]
fn empty_input_file() -> Result<(), Box<dyn std::error::Error>> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str("")?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(file.path());
  cmd.assert().success();

  Ok(())
}

#[test]
fn output_file_option() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let outfile = assert_fs::NamedTempFile::new("test.out")?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg("--output-file").arg(outfile.path());
  cmd.assert().success();
  outfile.assert(predicate::str::contains(indoc! {r#"
        .foo {
          border: none;
        }"#}));

  Ok(())
}

#[test]
fn output_file_option_create_missing_directories() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let outdir = assert_fs::TempDir::new()?;
  let outfile = outdir.child("out.css");
  outdir.close()?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg("--output-file").arg(outfile.path());
  cmd.assert().success();
  outfile.assert(predicate::str::contains(indoc! {
    r#"
      .foo {
        border: none;
      }
    "#
  }));
  fs::remove_dir_all(outfile.parent().unwrap())?;

  Ok(())
}

#[test]
fn multiple_input_files() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let infile2 = test_file2()?;
  let outdir = assert_fs::TempDir::new()?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg(infile2.path());
  cmd.arg("--output-dir").arg(outdir.path());
  cmd.assert().success();
  outdir
    .child(infile.file_name().unwrap())
    .assert(predicate::str::contains(indoc! {r#"
        .foo {
          border: none;
        }"#}));
  outdir
    .child(infile2.file_name().unwrap())
    .assert(predicate::str::contains(indoc! {r#"
        .foo {
          color: #ff0;
        }"#}));

  Ok(())
}

#[test]
fn multiple_input_files_out_file() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let infile2 = test_file2()?;
  let outdir = assert_fs::TempDir::new()?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg(infile2.path());
  cmd.arg("--output-file").arg(outdir.path());
  cmd.assert().failure();

  Ok(())
}

#[test]
fn multiple_input_files_stdout() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let infile2 = test_file2()?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg(infile2.path());
  cmd.assert().failure();

  Ok(())
}

#[test]
fn minify_option() -> Result<(), Box<dyn std::error::Error>> {
  let infile = test_file()?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg("--minify");
  cmd
    .assert()
    .success()
    .stdout(predicate::str::contains(indoc! {r#".foo{border:none}"#}));

  Ok(())
}

#[test]
fn nesting_option() -> Result<(), Box<dyn std::error::Error>> {
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  infile.write_str(
    r#"
        .foo {
          color: blue;
          & > .bar { color: red; }
        }
      "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg("--targets=defaults");
  cmd.arg("--nesting");
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
        .foo {
          color: #00f;
        }

        .foo > .bar {
          color: red;
        }
      "#}));

  Ok(())
}

#[test]
fn css_modules_infer_output_file() -> Result<(), Box<dyn std::error::Error>> {
  let (input, _, exports) = css_module_test_vals();
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  let outfile = assert_fs::NamedTempFile::new("out.css")?;
  infile.write_str(&input)?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(infile.path().parent().unwrap());
  cmd.arg(infile.path());
  cmd.arg("--css-modules");
  cmd.arg("-o").arg(outfile.path());
  cmd.assert().success();

  let expected: serde_json::Value = serde_json::from_str(&exports)?;
  let actual: serde_json::Value =
    serde_json::from_str(&std::fs::read_to_string(outfile.path().with_extension("json"))?)?;
  assert_eq!(expected, actual);

  Ok(())
}

#[test]
fn css_modules_output_target_option() -> Result<(), Box<dyn std::error::Error>> {
  let (input, _, exports) = css_module_test_vals();
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  let outfile = assert_fs::NamedTempFile::new("out.css")?;
  let modules_file = assert_fs::NamedTempFile::new("module.json")?;
  infile.write_str(&input)?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(infile.path().parent().unwrap());
  cmd.arg(infile.path());
  cmd.arg("-o").arg(outfile.path());
  cmd.arg("--css-modules").arg(modules_file.path());
  cmd.assert().success();

  let expected: serde_json::Value = serde_json::from_str(&exports)?;
  let actual: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(modules_file.path())?)?;
  assert_eq!(expected, actual);

  Ok(())
}

#[test]
fn css_modules_stdout() -> Result<(), Box<dyn std::error::Error>> {
  let (input, out_code, exports) = css_module_test_vals();
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  infile.write_str(&input)?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(infile.path().parent().unwrap());
  cmd.arg(infile.path());
  cmd.arg("--css-modules");
  let assert = cmd.assert().success();
  let output = assert.get_output();

  let expected: serde_json::Value = serde_json::from_str(&exports)?;
  let actual: serde_json::Value = serde_json::from_slice(&output.stdout)?;
  assert_eq!(out_code, actual.pointer("/code").unwrap().as_str().unwrap());
  assert_eq!(&expected, actual.pointer("/exports").unwrap());

  Ok(())
}

#[test]
fn css_modules_pattern() -> Result<(), Box<dyn std::error::Error>> {
  let (input, _, _) = css_module_test_vals();
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  infile.write_str(&input)?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(infile.path().parent().unwrap());
  cmd.arg(infile.path());
  cmd.arg("--css-modules");
  cmd.arg("--css-modules-pattern").arg("[name]-[hash]-[local]");
  cmd.assert().success().stdout(predicate::str::contains("test-EgL3uq-foo"));

  Ok(())
}

#[test]
fn css_modules_next_64299() -> Result<(), Box<dyn std::error::Error>> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str(
    "
  .blue {
    background: blue;

    :global {
      .red {
        background: red;
      }
    }

    &:global {
      &.green {
        background: green;
      }
    }
  }
  ",
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(file.path());
  cmd.arg("--css-modules");
  cmd.assert().failure();

  Ok(())
}

#[test]
fn sourcemap() -> Result<(), Box<dyn std::error::Error>> {
  let (input, _, _) = css_module_test_vals();
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  let outdir = assert_fs::TempDir::new()?;
  let outfile = outdir.child("out.css");
  infile.write_str(&input)?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(infile.path().parent().unwrap());
  cmd.arg(infile.path());
  cmd.arg("-o").arg(outfile.path());
  cmd.arg("--sourcemap");
  cmd.assert().success();

  outfile.assert(predicate::str::contains(&format!(
    "/*# sourceMappingURL={}.map */",
    outfile.path().to_str().unwrap()
  )));
  let mapfile = outdir.child("out.css.map");
  mapfile.assert(predicate::str::contains(r#""version":3"#));
  mapfile.assert(predicate::str::contains(r#""sources":["test.css"]"#));
  mapfile.assert(predicate::str::contains(
    r#""mappings":"AACM;;;;AAIA;;;;AAIA;;;;;;;;;;AAKA;;;;AAIA;;;;AAIA""#,
  ));

  Ok(())
}

#[test]
fn targets() -> Result<(), Box<dyn std::error::Error>> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str(
    r#"
      @custom-media --foo print;
      @media (--foo) {
        .a { color: red }
      }
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(file.path());
  cmd.arg("--custom-media");
  cmd.arg("--targets").arg("last 1 Chrome version");
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
        @media print {
          .a {
            color: red;
          }
        }"#}));

  Ok(())
}

#[test]
fn preserve_custom_media() -> Result<(), Box<dyn std::error::Error>> {
  let file = assert_fs::NamedTempFile::new("test.css")?;
  file.write_str(
    r#"
      @custom-media --foo print;
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(file.path());
  cmd.arg("--custom-media");
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    @custom-media --foo print;
  "#}));

  Ok(())
}

#[test]
/// Test command line argument parsing failing when `--targets` is used at the same time as `--browserslist`.
/// The two options are mutually exclusive.
fn browserslist_targets_exclusive() -> Result<(), Box<dyn std::error::Error>> {
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg("--targets").arg("defaults");
  cmd.arg("--browserslist");
  cmd.assert().failure().stderr(predicate::str::contains(indoc! {r#"
    error: The argument '--targets <TARGETS>' cannot be used with '--browserslist'
  "#}));

  Ok(())
}

#[test]
/// Test browserslist defaults being applied when no configuration is provided or discovered.
///
/// Note: This test might fail in unhygienic environments and should ideally run inside a chroot.
/// We have no control over the contents of our temp dir's parent dir (e.g. `/tmp`).
/// If this parent dir or its ancestors contain a `browserslist`, `.browserslistrc` or `package.json`
/// file, then configuration will be read from there, instead of applying defaults.
fn browserslist_defaults() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        -webkit-border-radius: 1rem;
        border-radius: 1rem;
      }
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test browserslist configuration being read from the `BROWSERSLIST` environment variable.
fn browserslist_env_config() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.env("BROWSERSLIST", "safari 4");
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test browserslist configuration being read from the file configured
/// by setting the `BROWSERSLIST_CONFIG` environment variable.
fn browserslist_env_config_file() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child("config");
  config.write_str(
    r#"
      safari 4
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.env("BROWSERSLIST_CONFIG", config.path());
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test `browserslist` configuration file being read.
fn browserslist_config_discovery() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child("browserslist");
  config.write_str(
    r#"
      safari 4
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test `.browserslistrc` configuration file being read.
fn browserslist_rc_discovery() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child(".browserslistrc");
  config.write_str(
    r#"
      safari 4
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test `package.json` configuration section being read.
fn browserslist_package_discovery() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child("package.json");
  config.write_str(
    r#"
      {
        "browserslist": "safari 4"
      }
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test environment targets being applied from the `NODE_ENV` environment variable.
fn browserslist_environment_from_node_env() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child("browserslist");
  config.write_str(
    r#"
      last 1 Chrome version

      [legacy]
      safari 4
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.env("NODE_ENV", "legacy");
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
/// Test environment targets being applied from the `BROWSERSLIST_ENV` environment variable.
fn browserslist_environment_from_browserslist_env() -> Result<(), Box<dyn std::error::Error>> {
  let dir = assert_fs::TempDir::new()?;
  let file = dir.child("test.css");
  file.write_str(
    r#"
      * {
        border-radius: 1rem;
      }
    "#,
  )?;

  let config = dir.child("browserslist");
  config.write_str(
    r#"
      last 1 Chrome version

      [legacy]
      safari 4
    "#,
  )?;

  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.current_dir(dir.path());
  cmd.env_clear();
  cmd.env("BROWSERSLIST_ENV", "legacy");
  cmd.arg("--browserslist");
  cmd.arg(file.path());
  cmd.assert().success().stdout(predicate::str::contains(indoc! {r#"
    * {
      -webkit-border-radius: 1rem;
      border-radius: 1rem;
    }
  "#}));

  Ok(())
}

#[test]
fn next_66191() -> Result<(), Box<dyn std::error::Error>> {
  let infile = assert_fs::NamedTempFile::new("test.css")?;
  infile.write_str(
    r#"
      .cb:is(input:checked) {
        margin: 3rem;
      }
    "#,
  )?;
  let outfile = assert_fs::NamedTempFile::new("test.out")?;
  let mut cmd = Command::cargo_bin("lightningcss")?;
  cmd.arg(infile.path());
  cmd.arg("--output-file").arg(outfile.path());
  cmd.assert().success();
  outfile.assert(predicate::str::contains(indoc! {r#".cb:is(input:checked)"#}));

  Ok(())
}
