#![cfg(feature = "bin")]
use chrono::Utc;
use cpp_linter::{
    rest_client::{COMMENT_MARKER, USER_OUTREACH},
    run::run_main,
};
use git_bot_feedback::LinesChangedOnly;
use mockito::Matcher;
use std::{env, io::Write, path::Path};
use tempfile::NamedTempFile;

mod common;
use common::{create_test_space, mock_server};

const SHA: &str = "8d68756375e0483c7ac2b4d6bbbece420dbbb495";
const REPO: &str = "cpp-linter/test-cpp-linter-action";
const PR: i64 = 27;
const TOKEN: &str = "123456";
const MOCK_ASSETS_PATH: &str = "tests/reviews_test_assets/";

const RESET_RATE_LIMIT_HEADER: &str = "x-ratelimit-reset";
const REMAINING_RATE_LIMIT_HEADER: &str = "x-ratelimit-remaining";

struct TestParams {
    pub lines_changed_only: LinesChangedOnly,
    pub tidy_review: bool,
    pub format_review: bool,
    pub passive_reviews: bool,
    pub no_lgtm: bool,
    pub force_lgtm: bool,
    pub summary_only: bool,
    pub pr_state: String,
    pub pr_draft: bool,
    pub fail_dismissal: bool,
    pub fail_get_existing_reviews: bool,
    pub fail_posting: bool,
    pub bad_pr_info: bool,
    pub bad_existing_reviews: bool,
}

impl Default for TestParams {
    fn default() -> Self {
        Self {
            lines_changed_only: LinesChangedOnly::On,
            tidy_review: true,
            format_review: true,
            passive_reviews: false,
            no_lgtm: true,
            force_lgtm: false,
            summary_only: false,
            pr_state: "open".to_string(),
            pr_draft: false,
            fail_dismissal: false,
            fail_get_existing_reviews: false,
            fail_posting: false,
            bad_pr_info: false,
            bad_existing_reviews: false,
        }
    }
}

fn generate_tool_summary(review_enabled: bool, force_lgtm: bool, tool_name: &str) -> String {
    if !review_enabled {
        return String::new();
    }
    if force_lgtm {
        format!("No concerns reported by {}. Great job! :tada:", tool_name)
    } else {
        format!("Click here for the full {} patch", tool_name)
    }
}

async fn setup(lib_root: &Path, test_params: &TestParams) {
    let mut event_payload_path = NamedTempFile::new_in("./").unwrap();
    let event_payload = if test_params.bad_pr_info {
        "".to_string()
    } else {
        serde_json::json!({
            "pull_request": {
                "draft": test_params.pr_draft,
                "state": test_params.pr_state,
                "number": PR,
                "locked": false,
            }
        })
        .to_string()
    };
    event_payload_path
        .write_all(event_payload.as_bytes())
        .expect("Failed to create mock event payload.");
    let tmp_out = NamedTempFile::new().unwrap();
    unsafe {
        env::set_var("GITHUB_ACTIONS", "true");
        env::set_var("GITHUB_OUTPUT", tmp_out.path());
        env::set_var("GITHUB_EVENT_NAME", "pull_request");
        env::set_var("GITHUB_REPOSITORY", REPO);
        env::set_var("GITHUB_SHA", SHA);
        env::set_var("GITHUB_TOKEN", TOKEN);
        env::set_var("CI", "true");
        if test_params.summary_only {
            env::set_var("CPP_LINTER_PR_REVIEW_SUMMARY_ONLY", "true");
        }
        env::set_var("GITHUB_EVENT_PATH", event_payload_path.path());
    }
    let clang_version = env::var("CLANG_VERSION").unwrap_or("".to_string());
    let reset_timestamp = (Utc::now().timestamp() + 60).to_string();
    let asset_path = format!("{}/{MOCK_ASSETS_PATH}", lib_root.to_str().unwrap());

    let mut server = mock_server().await;
    unsafe {
        env::set_var("GITHUB_API_URL", server.url());
    }
    let mut mocks = vec![];

    if !test_params.bad_pr_info {
        let pr_endpoint = format!("/repos/{REPO}/pulls/{PR}");
        mocks.push(
            server
                .mock("GET", format!("{pr_endpoint}/files").as_str())
                .match_header("Accept", "application/vnd.github.raw+json")
                .match_header("Authorization", format!("token {TOKEN}").as_str())
                .match_query(Matcher::Any)
                .with_body_from_file(format!("{asset_path}pr_27.json"))
                .with_header(REMAINING_RATE_LIMIT_HEADER, "50")
                .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
                .create(),
        );
    }

    let reviews_endpoint = format!("/repos/{REPO}/pulls/{PR}/reviews");

    if test_params.pr_state != "closed" && !test_params.bad_pr_info {
        let mut mock = server
            .mock("GET", reviews_endpoint.as_str())
            .match_header("Accept", "application/vnd.github.raw+json")
            .match_header("Authorization", format!("token {TOKEN}").as_str())
            .match_body(Matcher::Any)
            .match_query(Matcher::UrlEncoded("page".to_string(), "1".to_string()))
            .with_header(REMAINING_RATE_LIMIT_HEADER, "50")
            .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
            .with_status(if test_params.fail_get_existing_reviews {
                403
            } else {
                200
            });
        if test_params.bad_existing_reviews {
            mock = mock.with_body(String::new()).create();
        } else {
            mock = mock
                .with_body_from_file(format!("{asset_path}pr_reviews.json"))
                .create()
        }
        mocks.push(mock);
    }
    if !test_params.fail_get_existing_reviews
        && !test_params.bad_existing_reviews
        && test_params.pr_state != "closed"
        && !test_params.bad_pr_info
    {
        mocks.push(
            server
                .mock(
                    "PUT",
                    format!("{reviews_endpoint}/1807607546/dismissals").as_str(),
                )
                .match_body(r#"{"event":"DISMISS","message":"outdated review"}"#)
                .match_header("Authorization", format!("token {TOKEN}").as_str())
                .with_header(REMAINING_RATE_LIMIT_HEADER, "50")
                .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
                .with_status(if test_params.fail_dismissal { 403 } else { 200 })
                .create(),
        );
    }

    let lgtm_allowed = !test_params.force_lgtm || !test_params.no_lgtm;
    if lgtm_allowed
        && !test_params.pr_draft
        && test_params.pr_state == "open"
        && !test_params.bad_pr_info
    {
        let review_reaction = if test_params.passive_reviews {
            "COMMENT"
        } else if test_params.force_lgtm {
            "APPROVE"
        } else {
            "REQUEST_CHANGES"
        };
        let tidy_summary = generate_tool_summary(
            test_params.tidy_review,
            test_params.force_lgtm,
            "clang-tidy",
        );
        let format_summary = generate_tool_summary(
            test_params.format_review,
            test_params.force_lgtm,
            "clang-format",
        );
        let review_summary = format!(
            "{}## Cpp-linter Review.*{format_summary}.*{tidy_summary}.*{}",
            regex::escape(format!("{}", COMMENT_MARKER.escape_default()).as_str()),
            regex::escape(format!("{}", USER_OUTREACH.escape_default()).as_str()),
        );
        let expected_review_payload = format!(
            "\\{{\"event\":\"{review_reaction}\",\"body\":\"{review_summary}\",\"comments\":\\[{}\\]}}",
            if test_params.force_lgtm || test_params.summary_only {
                ""
            } else {
                ".+"
            }
        );
        mocks.push(
            server
                .mock("POST", reviews_endpoint.as_str())
                .match_body(Matcher::Regex(expected_review_payload))
                .match_header("Authorization", format!("token {TOKEN}").as_str())
                .with_header(REMAINING_RATE_LIMIT_HEADER, "50")
                .with_header(RESET_RATE_LIMIT_HEADER, reset_timestamp.as_str())
                .with_status(if test_params.fail_posting { 403 } else { 200 })
                .create(),
        );
    }

    let mut format_ignore = "**/*.c".to_string();
    let mut tidy_ignore = format_ignore.clone();
    if test_params.force_lgtm {
        format_ignore.push_str("|**/*.cpp|**/*.h");
        tidy_ignore.push_str("|**/*.hpp");
    }
    let mut args = vec![
        "cpp-linter".to_string(),
        "-v=debug".to_string(),
        format!("-V={}", clang_version),
        format!("-l={}", test_params.lines_changed_only),
        format!("--ignore-tidy={}", tidy_ignore),
        format!("--ignore-format={}", format_ignore),
        format!("--tidy-review={}", test_params.tidy_review),
        format!("--format-review={}", test_params.format_review),
        format!("--passive-reviews={}", test_params.passive_reviews),
        format!("--no-lgtm={}", test_params.no_lgtm),
        "-p=build".to_string(),
        "-i=build".to_string(),
    ];
    if test_params.force_lgtm {
        if test_params.tidy_review {
            // only use a check that doesn't trigger concern on test assets
            args.push("--tidy-checks=-*,bugprone-infinite-loop".to_string());
        }
        if test_params.format_review {
            // explicitly disable formatting using `DisableFormat: true`
            args.push("--style={DisableFormat: true}".to_string());
        }
    } else {
        args.push("--style=file".to_string()); // use .clang-format file
    }
    match run_main(args).await {
        Ok(_) => {
            if test_params.bad_existing_reviews || test_params.bad_pr_info {
                panic!("Expected failure, but it succeeded");
            }
        }
        Err(e) => {
            if !test_params.bad_existing_reviews && !test_params.bad_pr_info {
                panic!("Failed unexpectedly: {e:?}");
            }
        }
    }
    for mock in mocks {
        mock.assert();
    }
}

async fn test_review(test_params: &TestParams) {
    let tmp_dir = create_test_space(true);
    let lib_root = env::current_dir().unwrap();
    env::set_current_dir(tmp_dir.path()).unwrap();
    setup(&lib_root, test_params).await;
    env::set_current_dir(lib_root.as_path()).unwrap();
    drop(tmp_dir);
}

#[tokio::test]
async fn all_lines() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn all_lines_tidy_only() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        format_review: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn all_lines_format_only() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        tidy_review: false,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn changed_lines() {
    test_review(&TestParams::default()).await;
}

#[tokio::test]
async fn all_lines_passive() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        passive_reviews: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn changed_lines_passive() {
    test_review(&TestParams {
        passive_reviews: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn summary_only() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        summary_only: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn lgtm() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        no_lgtm: false,
        force_lgtm: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn lgtm_passive() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        no_lgtm: false,
        force_lgtm: true,
        passive_reviews: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn no_lgtm() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        force_lgtm: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn is_draft() {
    test_review(&TestParams {
        pr_draft: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn is_closed() {
    test_review(&TestParams {
        pr_state: "closed".to_string(),
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn fail_posting() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        fail_posting: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn fail_dismissal() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        fail_dismissal: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn fail_get_existing_reviews() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        fail_get_existing_reviews: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn bad_existing_reviews() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        force_lgtm: true,
        bad_existing_reviews: true,
        ..Default::default()
    })
    .await;
}

#[tokio::test]
async fn bad_pr_info() {
    test_review(&TestParams {
        lines_changed_only: LinesChangedOnly::Off,
        force_lgtm: true,
        bad_pr_info: true,
        ..Default::default()
    })
    .await;
}
