use http_body_util::BodyExt;
use azumi::action::error_fragment;

// Single source of truth — same minify_js as build.rs
include!("../build_support/minify_js.rs");

async fn response_to_string(response: axum::response::Response) -> String {
    let body = response.into_body();
    let bytes = body.collect().await.unwrap().to_bytes();
    String::from_utf8(bytes.to_vec()).unwrap()
}

mod minify_js_tests {
    use super::*;

    #[test]
    fn strips_line_comments() {
        let src = "let x = 1; // this is a comment\nlet y = 2;";
        let result = minify_js(src);
        assert!(!result.contains("comment"));
        assert!(result.contains("let x = 1;"));
        assert!(result.contains("let y = 2;"));
    }

    #[test]
    fn strips_block_comments() {
        let src = "let x = 1; /* block comment */ let y = 2;";
        let result = minify_js(src);
        assert!(!result.contains("block comment"));
        assert!(result.contains("let x = 1;"));
        assert!(result.contains("let y = 2;"));
    }

    #[test]
    fn preserves_string_literals() {
        let src = r#"let s = "// not a comment"; let t = '/* not block */';"#;
        let result = minify_js(src);
        assert!(result.contains("// not a comment"));
        assert!(result.contains("/* not block */"));
    }

    #[test]
    fn preserves_template_literals() {
        let src = "let s = `hello ${world}`;";
        let result = minify_js(src);
        assert!(result.contains("hello ${world}"));
    }

    #[test]
    fn collapses_whitespace() {
        let src = "let   x   =   1;\n\n\nlet   y   =   2;";
        let result = minify_js(src);
        assert!(!result.contains("   "));
        assert!(!result.contains("\n\n"));
    }

    #[test]
    fn empty_input_falls_back() {
        let src = "   \n   \n   ";
        let result = minify_js(src);
        assert_eq!(result, src);
    }

    #[test]
    fn handles_escaped_quotes_in_strings() {
        let src = r#"let s = "he said \"hello\"";"#;
        let result = minify_js(src);
        assert!(result.contains(r#"he said \"hello\""#));
    }

    #[test]
    fn no_false_positive_division_as_comment() {
        let src = "let x = 10 / 2; // real comment";
        let result = minify_js(src);
        assert!(result.contains("10 / 2"), "division operator should survive");
        assert!(!result.contains("real comment"), "actual comment should be stripped");
    }

    #[test]
    fn preserves_regex_literal() {
        let src = "let re = /foo/;";
        let result = minify_js(src);
        assert!(result.contains("/foo/"), "regex literal should be preserved");
    }

    #[test]
    fn preserves_regex_with_flags() {
        let src = "let re = /pattern/gi;";
        let result = minify_js(src);
        assert!(result.contains("/pattern/gi"), "regex with flags should be preserved");
    }

    #[test]
    fn preserves_regex_with_escaped_slash() {
        let src = r"let re = /https?:\/\//;";
        let result = minify_js(src);
        assert!(result.contains("/https?:\\/"), "regex with escaped slash should be preserved");
    }

    #[test]
    fn preserves_regex_in_test_call() {
        let src = "if (/pattern/.test(s)) {}";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex in test() call should be preserved");
    }

    #[test]
    fn regex_after_semicolon() {
        let src = "return /abc/;";
        let result = minify_js(src);
        assert!(result.contains("/abc/"), "regex after return keyword should be preserved");
    }

    #[test]
    fn regex_after_equals() {
        let src = "let x = /foo/.test(s);";
        let result = minify_js(src);
        assert!(result.contains("/foo/"), "regex after = should be preserved");
    }

    #[test]
    fn regex_with_char_class_containing_slash() {
        let src = "let re = /[/]/;";
        let result = minify_js(src);
        assert!(result.contains("[/"), "regex char class with / should be preserved");
    }

    #[test]
    fn division_not_treated_as_regex() {
        let src = "let x = a / b;";
        let result = minify_js(src);
        assert!(result.contains("a / b"), "division should not become regex");
    }

    #[test]
    fn block_comment_at_eof_no_panic() {
        let src = "let x = 1; /* unclosed";
        let result = minify_js(src);
        assert!(result.contains("let x = 1;"), "code before unclosed comment should survive");
    }

    #[test]
    fn preserves_multibyte_utf8_in_strings() {
        let src = r#"let s = "café";"#;
        let result = minify_js(src);
        assert!(result.contains("café"), "multi-byte UTF-8 in strings should be preserved: {}", result);
    }

    #[test]
    fn preserves_cjk_in_code() {
        let src = r#"let 名前 = "日本語"; // comment"#;
        let result = minify_js(src);
        assert!(result.contains("名前"), "CJK identifier should be preserved: {}", result);
        assert!(result.contains("日本語"), "CJK string should be preserved: {}", result);
        assert!(!result.contains("comment"), "comment should be stripped");
    }

    #[test]
    fn preserves_emoji_in_strings() {
        let src = r#"let s = "hello 🌍";"#;
        let result = minify_js(src);
        assert!(result.contains("🌍"), "emoji in string should be preserved: {}", result);
    }

    #[test]
    fn regex_after_return_keyword() {
        let src = "return /pattern/;";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after return keyword should be preserved: {}", result);
    }

    #[test]
    fn regex_after_typeof_keyword() {
        let src = "typeof /pattern/";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after typeof keyword should be preserved: {}", result);
    }

    #[test]
    fn regex_after_throw_keyword() {
        let src = "throw /pattern/";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after throw keyword should be preserved: {}", result);
    }

    #[test]
    fn regex_after_delete_keyword() {
        let src = "delete /pattern/";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after delete keyword should be preserved: {}", result);
    }

    #[test]
    fn regex_after_void_keyword() {
        let src = "void /pattern/";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after void keyword should be preserved: {}", result);
    }

    #[test]
    fn identifier_prevents_regex() {
        let src = "foo / bar;";
        let result = minify_js(src);
        assert!(result.contains("foo / bar"), "identifier before / should be division, not regex: {}", result);
    }

    #[test]
    fn closing_paren_prevents_regex() {
        let src = "foo() / 2;";
        let result = minify_js(src);
        assert!(result.contains("() / 2"), ") before / should be division: {}", result);
    }

    #[test]
    fn return_regex_with_double_slash() {
        let src = "return /https://example/;";
        let result = minify_js(src);
        assert!(result.contains("/https://example/"), "regex with :// after return should not be mangled as comment: {}", result);
    }

    #[test]
    fn number_then_division() {
        let src = "let x = 10 / 2; // real comment";
        let result = minify_js(src);
        assert!(result.contains("10 / 2"), "number / number should be division: {}", result);
        assert!(!result.contains("real comment"), "actual comment should be stripped");
    }

    #[test]
    fn regex_after_in_keyword() {
        let src = "for (var k in /pattern/.test(x)) {}";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after 'in' keyword should be preserved: {}", result);
    }

    #[test]
    fn regex_after_case_keyword() {
        let src = "case /pattern/:";
        let result = minify_js(src);
        assert!(result.contains("/pattern/"), "regex after 'case' keyword should be preserved: {}", result);
    }
}

mod error_fragment_tests {
    use super::*;

    #[tokio::test]
    async fn uses_az_on_instead_of_onclick() {
        let response = error_fragment("Error", Some("my_form"));
        let body = response_to_string(response).await;
        assert!(
            body.contains("az-on="),
            "should use az-on: {}",
            body
        );
        assert!(
            !body.contains("onclick="),
            "should NOT use onclick: {}",
            body
        );
    }

    #[tokio::test]
    async fn retry_button_calls_builtin_action() {
        let response = error_fragment("Error", Some("my_form"));
        let body = response_to_string(response).await;
        assert!(
            body.contains("__azumi_retry"),
            "should reference __azumi_retry: {}",
            body
        );
    }

    #[tokio::test]
    async fn stores_form_id_in_data_attribute() {
        let response = error_fragment("Error", Some("my_form"));
        let body = response_to_string(response).await;
        assert!(
            body.contains("data-retry-form=\"my_form\""),
            "should have data-retry-form: {}",
            body
        );
    }

    #[tokio::test]
    async fn no_form_id_means_no_retry_button() {
        let response = error_fragment("Error", None);
        let body = response_to_string(response).await;
        assert!(
            !body.contains("__azumi_retry"),
            "no retry button without form_id: {}",
            body
        );
        assert!(
            !body.contains("data-retry-form"),
            "no data-retry-form without form_id: {}",
            body
        );
    }
}

mod js_client_tests {
    #[test]
    fn js_retry_tests_pass() {
        let output = std::process::Command::new("node")
            .arg("client/tests/retry.test.js")
            .current_dir(env!("CARGO_MANIFEST_DIR"))
            .output()
            .expect("failed to run node; is Node.js installed?");
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        assert!(
            output.status.success(),
            "JS retry tests failed:\n{}\n{}",
            stdout,
            stderr
        );
    }
}
