use rayon::prelude::*;
use std::{
    fs::create_dir_all,
    path::{Path, PathBuf},
};
use swc::{
    config::{
        BuiltConfig, Config, JscConfig, ModuleConfig, Options, SourceMapsConfig, TransformConfig,
    },
    Compiler, TransformOutput,
};
use swc_common::{chain, comments::Comment, BytePos, FileName};
use swc_ecma_ast::{EsVersion, *};
use swc_ecma_parser::{EsConfig, Syntax, TsConfig};
use swc_ecma_transforms::{
    helpers::{self, Helpers},
    pass::noop,
};
use swc_ecma_utils::HANDLER;
use swc_ecma_visit::{Fold, FoldWith};
use testing::{NormalizedOutput, StdErr, Tester};
use walkdir::WalkDir;

fn file(filename: &str) -> Result<NormalizedOutput, StdErr> {
    file_with_opt(
        filename,
        Options {
            swcrc: true,
            ..Default::default()
        },
    )
}

fn file_with_opt(filename: &str, options: Options) -> Result<NormalizedOutput, StdErr> {
    Tester::new().print_errors(|cm, handler| {
        let c = Compiler::new(cm.clone());

        let fm = cm
            .load_file(Path::new(filename))
            .expect("failed to load file");
        let s = c.process_js_file(
            fm,
            &handler,
            &Options {
                is_module: true,
                ..options
            },
        );

        match s {
            Ok(v) => {
                if handler.has_errors() {
                    Err(())
                } else {
                    Ok(v.code.into())
                }
            }
            Err(err) => panic!("Error: {:?}", err),
        }
    })
}

fn str_with_opt(content: &str, options: Options) -> Result<NormalizedOutput, StdErr> {
    compile_str(FileName::Anon, content, options).map(|v| v.code.into())
}

fn compile_str(
    filename: FileName,
    content: &str,
    options: Options,
) -> Result<TransformOutput, StdErr> {
    Tester::new().print_errors(|cm, handler| {
        let c = Compiler::new(cm.clone());

        let fm = cm.new_source_file(filename, content.to_string());
        let s = c.process_js_file(
            fm,
            &handler,
            &Options {
                is_module: true,
                ..options
            },
        );

        match s {
            Ok(v) => {
                if handler.has_errors() {
                    Err(())
                } else {
                    Ok(v)
                }
            }
            Err(err) => panic!("Error: {:?}", err),
        }
    })
}

fn project(dir: &str) {
    Tester::new()
        .print_errors(|cm, handler| {
            let c = Compiler::new(cm.clone());

            for entry in WalkDir::new(dir) {
                let entry = entry.unwrap();
                if entry.metadata().unwrap().is_dir() {
                    continue;
                }
                println!("File: {}", entry.path().to_string_lossy());

                if !entry.file_name().to_string_lossy().ends_with(".ts")
                    && !entry.file_name().to_string_lossy().ends_with(".js")
                    && !entry.file_name().to_string_lossy().ends_with(".tsx")
                {
                    continue;
                }

                let fm = cm.load_file(entry.path()).expect("failed to load file");

                if c.config_for_file(
                    &handler,
                    &Options {
                        swcrc: true,
                        is_module: true,

                        ..Default::default()
                    },
                    &fm.name,
                    noop(),
                )
                .expect("failed to read config")
                .is_none()
                {
                    continue;
                }

                match c.process_js_file(
                    fm,
                    &handler,
                    &Options {
                        swcrc: true,
                        is_module: true,

                        ..Default::default()
                    },
                ) {
                    Ok(..) => {}
                    Err(ref err) if format!("{:?}", err).contains("not matched") => {}
                    Err(err) => panic!("Error: {:?}", err),
                }
            }

            Ok(())
        })
        .map(|_| ())
        .expect("failed");
}

fn par_project(dir: &str) {
    Tester::new()
        .print_errors(|cm, handler| {
            let c = Compiler::new(cm.clone());

            let entries = WalkDir::new(dir)
                .into_iter()
                .map(|entry| entry.unwrap())
                .filter(|e| {
                    if e.metadata().unwrap().is_dir() {
                        return false;
                    }

                    if !e.file_name().to_string_lossy().ends_with(".ts")
                        && !e.file_name().to_string_lossy().ends_with(".js")
                        && !e.file_name().to_string_lossy().ends_with(".tsx")
                    {
                        return false;
                    }

                    true
                })
                .collect::<Vec<_>>();

            entries.into_par_iter().for_each(|entry| {
                let fm = cm.load_file(entry.path()).expect(
                    "failed to load
 file",
                );
                let _ = c.process_js_file(
                    fm,
                    &handler,
                    &Options {
                        swcrc: true,
                        is_module: true,
                        source_maps: Some(SourceMapsConfig::Bool(true)),
                        ..Default::default()
                    },
                );
            });

            if handler.has_errors() {
                Err(())
            } else {
                Ok(())
            }
        })
        .map(|_| ())
        .expect("");
}

/// should respect modules config in .swcrc
#[test]
fn issue_225() {
    let s = file("tests/projects/issue-225/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains("function _interopRequireDefault"));
    assert!(s.contains("var _foo = _interopRequireDefault(require(\"foo\"))"));
}

/// should handle exportNamespaceFrom configured by .swcrc
#[test]
fn issue_226() {
    let s = file("tests/projects/issue-226/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains("import * as _Foo from 'bar';"));
    assert!(s.contains("export { _Foo as Foo }"));
}

/// should handle react correctly
#[test]
fn issue_351() {
    let s = file("tests/projects/issue-351/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains(".default.createElement(\"div\", null);"));
}

/// should handle cjs imports
#[test]
fn issue_389() {
    let s = file("tests/projects/issue-389/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains(".default.bar = true"));
}

/// should handle comments in arrow expression
#[test]
fn issue_406() {
    let s = file("tests/projects/issue-406/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains("return true"));
}

#[test]
fn issue_409_1() {
    let s = file("tests/projects/issue-409-1/input.js").unwrap();
    println!("{}", s);

    assert!(s.contains("JSON.parse"));
}

#[test]
fn issue_409_2() {
    let s = file("tests/projects/issue-409-2/input.js").unwrap();
    println!("{}", s);

    assert!(!s.contains("JSON.parse"));
}

/// should handle multiple entries in swcrc
#[test]
fn issue_414() {
    let s1 = file("tests/projects/issue-414/a.js").unwrap();
    println!("{}", s1);
    assert!(s1.contains("require(\"foo\")"));

    let s2 = file("tests/projects/issue-414/b.ts").unwrap();
    println!("{}", s2);
    assert!(s2.contains("define("));
    assert!(s2.contains("function(_bar) {"));
}

/// should handle comments in return statement
#[test]
fn issue_415() {
    let s = file("tests/projects/issue-415/input.js").unwrap();

    assert!(s.replace(" ", "").contains("return(/*#__PURE__*/"));
}

#[test]
fn issue_466_1() {
    project("tests/projects/issue-466-1");
}

#[test]
fn issue_466_2() {
    project("tests/projects/issue-466-2");
}

#[test]
fn issue_467() {
    project("tests/projects/issue-467/");
}

#[test]
fn issue_468() {
    file("tests/projects/issue-468/input.ts").expect("failed to parse typescript");
}

#[test]
fn issue_528() {
    let f = file("tests/projects/issue-528/input.js")
        .unwrap()
        .replace(" ", "");
    let f = f.trim();

    println!("{}", f);
    assert_eq!(
        f,
        "\
//bar
[
//foo
a,
//bar
(//baz
b)
];"
    );
}

#[test]
fn env_entry_chrome_49() {
    let f = file("tests/env/entry/chrome-49/input.js")
        .unwrap()
        .replace(" ", "");
    let f = f.trim();

    println!("{}", f);

    assert_eq!(f.lines().count(), 78);
}

#[test]
fn env_entry_chrome_71() {
    let f = file("tests/env/entry/chrome-71/input.js")
        .unwrap()
        .replace(" ", "");
    let f = f.trim();

    println!("{}", f);

    assert_eq!(f.lines().count(), 7);
}

#[test]
fn env_query_chrome_71() {
    let f = file("tests/env/query/chrome-71/input.js")
        .unwrap()
        .replace(" ", "");
    let f = f.trim();

    println!("{}", f);

    assert_eq!(f.lines().count(), 7);
}

#[test]
fn project_env() {
    project("tests/projects/env/");
}

#[test]
fn issue_602() {
    let f = file("tests/projects/issue-602/input.js").unwrap();
    println!("{}", f);

    assert!(!f.contains("undefined1"));
}

#[test]
fn issue_604_1() {
    let f = file("tests/projects/issue-604-1/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("_loop(i)"));
}

#[test]
fn issue_605() {
    let f = file("tests/projects/issue-605/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("Object.keys(_c)"));
}

#[test]
fn await_expr() {
    let f = file("tests/projects/await-expression/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("await"));
    assert!(f.contains("test"));
}

#[test]
fn await_expr_2() {
    let f = file("tests/projects/await-expression-2/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("await"));
    assert!(f.contains("test"));
}

#[test]
fn default_config() {
    project("tests/projects/issue-655")
}

#[test]
fn issue_658() {
    project("tests/projects/issue-658")
}

#[test]
fn issue_763() {
    let f = file("tests/projects/issue-763/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("_consts.RESOURCE_WEBSITE"));
    assert!(f.contains("_consts.RESOURCE_FACEBOOK"));
    assert!(f.contains("_consts.RESOURCE_INSTAGRAM"));
}

#[test]
fn issue_763_2() {
    let f = file("tests/projects/issue-763-2/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("_consts.RESOURCE_WEBSITE"));
    assert!(f.contains("_consts.RESOURCE_FACEBOOK"));
    assert!(f.contains("_consts.RESOURCE_INSTAGRAM"));
}

#[test]
fn issue_779_1() {
    let f = file("tests/projects/issue-779-1/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("require(\"core-js/modules/es.array-buffer.constructor\");"))
}

#[test]
fn issue_779_2() {
    let f = file("tests/projects/issue-779-2/input.js").unwrap();
    println!("{}", f);

    assert!(f.contains("require(\"core-js\");"));
}

#[test]
fn issue_783() {
    let f = file("tests/projects/issue-783/input.js").unwrap();
    println!("{}", f);

    assert!(!f.contains("require(\"core-js\");"));
    assert!(f.contains("require(\"core-js/modules/es.array-buffer.constructor\");"))
}

#[test]
fn issue_783_core_js_2() {
    let f = file_with_opt(
        "tests/projects/issue-783/input.js",
        Options {
            swcrc: false,
            config: Config {
                env: Some(swc_ecma_preset_env::Config {
                    core_js: Some("2".parse().unwrap()),
                    mode: Some(swc_ecma_preset_env::Mode::Entry),
                    ..Default::default()
                }),
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", f);

    assert!(
        !f.contains("'core-js'"),
        "import of `core-js` should be transformed"
    );
}

#[test]
fn issue_783_core_js_3() {
    let f = file_with_opt(
        "tests/projects/issue-783/input.js",
        Options {
            swcrc: false,
            config: Config {
                env: Some(swc_ecma_preset_env::Config {
                    mode: Some(swc_ecma_preset_env::Mode::Entry),
                    ..Default::default()
                }),
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", f);

    assert!(
        !f.contains("'core-js'"),
        "import of `core-js` should be transformed"
    );
}

#[test]
fn issue_801() {
    let f = file("tests/projects/issue-801/input.ts").unwrap();
    println!("{}", f);

    assert!(!f.contains("function delete"));
}

#[test]
#[ignore]
fn concurrency() {
    par_project("tests/deno-unit");
}

#[test]
fn issue_879() {
    let f = file_with_opt(
        "tests/projects/issue-879/input.ts",
        Options {
            is_module: true,
            config: Config {
                env: Some(Default::default()),
                module: Some(ModuleConfig::CommonJs(Default::default())),
                jsc: JscConfig {
                    syntax: Some(Syntax::Typescript(TsConfig {
                        tsx: true,
                        decorators: true,
                        ..Default::default()
                    })),
                    transform: Some(TransformConfig {
                        legacy_decorator: true,
                        ..Default::default()
                    }),
                    ..Default::default()
                },
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", f);

    assert!(f.find("function X").is_some(), "swc should compile class");
    assert_eq!(
        f.find("function X"),
        f.rfind("function X"),
        "swc should not emit `function X` twice"
    );
}

#[test]
fn issue_895() {
    let f = file("tests/projects/issue-895/input.ts").unwrap();
    println!("{}", f);

    assert!(f.contains("_url ="));
    assert!(f.contains("(0, _url).queryString"));
    let s = f.replace("_url =", "").replace("_url.queryString", "");

    assert!(!s.contains("_url."));
    assert!(!s.contains("_url,"));
}

#[test]
fn issue_1052() {
    let f = file("tests/projects/issue-1052/input.ts").unwrap();
    println!("{}", f);

    assert!(!f.contains("_new"))
}

#[test]
fn issue_1203() {
    let f = file("tests/projects/issue-1203/input.js").unwrap();
    println!("{}", f);

    assert!(!f.contains("return //"))
}

#[test]
fn codegen_1() {
    let f = file("tests/projects/codegen-1/input.js").unwrap();
    println!("{}", f);

    assert_eq!(f.to_string(), "\"\\\"\";\n");
}

#[test]
fn issue_1549() {
    let output = str_with_opt(
        "const a = `\r\n`;",
        Options {
            is_module: true,
            config: Config {
                jsc: JscConfig {
                    target: Some(EsVersion::Es5),
                    ..Default::default()
                },
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", output);

    assert_eq!(output.to_string(), "var a = \"\\n\";\n");
}

#[test]
fn deno_10282_1() {
    let output = str_with_opt(
        "const a = `\r\n`;",
        Options {
            is_module: true,
            config: Config {
                jsc: JscConfig {
                    target: Some(EsVersion::Es3),
                    ..Default::default()
                },
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", output);

    assert_eq!(output.to_string(), "var a = \"\\n\";\n");
}

#[test]
fn deno_10282_2() {
    let output = str_with_opt(
        "const a = `\r\n`;",
        Options {
            is_module: true,
            config: Config {
                jsc: JscConfig {
                    target: Some(EsVersion::Es2020),
                    ..Default::default()
                },
                ..Default::default()
            },
            ..Default::default()
        },
    )
    .unwrap();
    println!("{}", output);

    assert_eq!(output.to_string(), "const a = `\n`;\n");
}

struct Panicking;

impl Fold for Panicking {
    fn fold_jsx_opening_element(&mut self, node: JSXOpeningElement) -> JSXOpeningElement {
        let JSXOpeningElement { name, .. } = &node;
        println!("HMM");

        if let JSXElementName::Ident(Ident { sym, .. }) = name {
            panic!("visited: {}", sym)
        }

        JSXOpeningElement { ..node }
    }
}

#[test]
#[should_panic = "visited"]
fn should_visit() {
    Tester::new()
        .print_errors(|cm, handler| {
            let c = Compiler::new(cm.clone());

            let fm = cm.new_source_file(
                FileName::Anon,
                "
                    import React from 'react';
                    const comp = () => <amp-something className='something' />;
                "
                .into(),
            );
            let config = c
                .config_for_file(
                    &handler,
                    &swc::config::Options {
                        config: swc::config::Config {
                            jsc: JscConfig {
                                syntax: Some(Syntax::Es(EsConfig {
                                    jsx: true,
                                    ..Default::default()
                                })),
                                ..Default::default()
                            },
                            ..Default::default()
                        },
                        ..Default::default()
                    },
                    &fm.name,
                    noop(),
                )
                .unwrap()
                .unwrap();

            dbg!(config.syntax);
            let program = c
                .parse_js(
                    fm.clone(),
                    &handler,
                    config.target,
                    config.syntax,
                    true,
                    true,
                )
                .map_err(|_| ())?;

            let config = BuiltConfig {
                pass: chain!(Panicking, config.pass),
                syntax: config.syntax,
                target: config.target,
                minify: config.minify,
                external_helpers: config.external_helpers,
                source_maps: config.source_maps,
                input_source_map: config.input_source_map,
                is_module: config.is_module,
                output_path: config.output_path,
                source_file_name: config.source_file_name,
                preserve_comments: config.preserve_comments,
                inline_sources_content: config.inline_sources_content,
            };

            if config.minify {
                let preserve_excl = |_: &BytePos, vc: &mut Vec<Comment>| -> bool {
                    vc.retain(|c: &Comment| c.text.starts_with("!"));
                    !vc.is_empty()
                };
                c.comments().leading.retain(preserve_excl);
                c.comments().trailing.retain(preserve_excl);
            }
            let mut pass = config.pass;
            let program = helpers::HELPERS.set(&Helpers::new(config.external_helpers), || {
                HANDLER.set(&handler, || {
                    // Fold module
                    program.fold_with(&mut pass)
                })
            });

            Ok(c.print(
                &program,
                None,
                config.output_path,
                config.inline_sources_content,
                config.target,
                config.source_maps,
                &[],
                None,
                // TODO: figure out sourcemaps
                config.minify,
                config.preserve_comments,
            )
            .unwrap()
            .code)
        })
        .unwrap();
}

#[testing::fixture("tests/fixture/**/input/")]
fn tests(input_dir: PathBuf) {
    let output = input_dir.parent().unwrap().join("output");

    Tester::new()
        .print_errors(|cm, handler| {
            let c = Compiler::new(cm.clone());

            for entry in WalkDir::new(&input_dir) {
                let entry = entry.unwrap();
                if entry.metadata().unwrap().is_dir() {
                    continue;
                }
                println!("File: {}", entry.path().to_string_lossy());

                if !entry.file_name().to_string_lossy().ends_with(".ts")
                    && !entry.file_name().to_string_lossy().ends_with(".js")
                    && !entry.file_name().to_string_lossy().ends_with(".tsx")
                {
                    continue;
                }

                let rel_path = entry
                    .path()
                    .strip_prefix(&input_dir)
                    .expect("failed to strip prefix");

                let fm = cm.load_file(entry.path()).expect("failed to load file");
                match c.process_js_file(
                    fm,
                    &handler,
                    &Options {
                        swcrc: true,
                        is_module: true,
                        output_path: Some(output.join(entry.file_name())),

                        ..Default::default()
                    },
                ) {
                    Ok(v) => {
                        NormalizedOutput::from(v.code)
                            .compare_to_file(output.join(rel_path))
                            .unwrap();

                        let _ = create_dir_all(output.join(rel_path).parent().unwrap());

                        let map = v.map.map(|json| {
                            let json: serde_json::Value = serde_json::from_str(&json).unwrap();
                            serde_json::to_string_pretty(&json).unwrap()
                        });

                        NormalizedOutput::from(map.unwrap_or_default())
                            .compare_to_file(
                                output.join(rel_path.with_extension("map").file_name().unwrap()),
                            )
                            .unwrap();
                    }
                    Err(ref err) if format!("{:?}", err).contains("not matched") => {}
                    Err(err) => panic!("Error: {:?}", err),
                }
            }

            Ok(())
        })
        .map(|_| ())
        .expect("failed");
}

#[test]
fn issue_1984() {
    testing::run_test2(false, |cm, handler| {
        let c = Compiler::new(cm.clone());
        let fm = c.cm.new_source_file(
            FileName::Anon,
            "
            function Set() {}
            function useSelection(selectionType, derivedHalfSelectedKeys) {
                return selectionType === 'radio'
                    ? new Set()
                    : new Set(derivedHalfSelectedKeys);
            }
            "
            .into(),
        );

        c.minify(fm, &handler, &serde_json::from_str("{}").unwrap())
            .unwrap();

        Ok(())
    })
    .unwrap()
}

#[test]
fn opt_source_file_name_1() {
    let map = compile_str(
        FileName::Real(PathBuf::from("not-unique.js")),
        "import Foo from 'foo';",
        Options {
            filename: "unique.js".into(),
            source_file_name: Some("entry-foo".into()),
            source_maps: Some(SourceMapsConfig::Bool(true)),
            ..Default::default()
        },
    )
    .unwrap()
    .map
    .unwrap();

    println!("{}", map);

    assert!(map.contains("entry-foo"));
}
