//! Login + whoami journeys beyond the happy path: bad tokens, missing
//! credentials file, stale on-disk creds, and the role-label rendered
//! by `pic login`.

use predicates::prelude::*;

use crate::common;

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn login_persists_credentials_with_correct_perms() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);

    common::pic_as(&env).args(["login"]).assert().success();

    let creds_path = env.config_dir.path().join("credentials");
    let body = std::fs::read_to_string(&creds_path).expect("credentials file");
    assert!(
        body.contains(&format!("url = \"{}\"", env.url)),
        "creds missing url line: {body}",
    );
    assert!(
        body.contains(&format!("token = \"{}\"", env.token)),
        "creds missing token line: {body}",
    );
    assert!(
        body.contains(&format!("username = \"{}\"", fx.admin_username)),
        "creds missing username line: {body}",
    );

    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        let mode = std::fs::metadata(&creds_path).unwrap().permissions().mode() & 0o777;
        assert_eq!(mode, 0o600, "credentials file must be 0600, got {mode:o}");
    }
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn login_rejects_bad_token() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::custom_env(&fx.url, "pic_garbage_token");

    common::pic_as(&env)
        .args(["login"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("401").or(predicate::str::contains("token rejected")));

    let creds_path = env.config_dir.path().join("credentials");
    assert!(
        !creds_path.exists(),
        "failed login must not persist credentials"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn whoami_without_credentials_errors() {
    let Some(_fx) = common::fixture_or_skip() else {
        return;
    };
    // Build a TestEnv directly so the config dir stays empty —
    // `admin_env` would seed a credentials file, masking the bug
    // this test is supposed to catch.
    let env = common::TestEnv {
        url: String::new(),
        token: String::new(),
        config_dir: tempfile::TempDir::new().unwrap(),
        home: tempfile::TempDir::new().unwrap(),
    };

    common::pic_no_env(&env)
        .args(["whoami"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("pic login"));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn whoami_with_stale_token_errors() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::admin_env(fx);

    let body = format!(
        "url = \"{}\"\ntoken = \"pic_stale_token\"\nusername = \"ghost\"\n",
        env.url
    );
    std::fs::write(env.config_dir.path().join("credentials"), body).unwrap();

    common::pic_no_env(&env)
        .args(["whoami"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("401").or(predicate::str::contains("token rejected")));
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn login_prints_member_role_label() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let username = common::unique_username("auth");
    let m = common::member::member_user(fx, &username);
    let env = common::custom_env(&fx.url, &m.token);

    common::pic_as(&env)
        .args(["login"])
        .assert()
        .success()
        .stdout(predicate::str::contains(format!(
            "Logged in as {} (member)",
            m.username
        )));
}

/// Drive the real username+password flow end-to-end. `pic_no_env`
/// strips `PICLOUD_TOKEN` so login can't short-circuit through the
/// bearer path; stdin feeds `username\npassword\n` (the URL is supplied
/// via `--url` to avoid the third prompt).
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn login_with_username_and_password_persists() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let username = common::unique_username("lpw");
    let m = common::member::member_user(fx, &username);
    let env = common::custom_env(&fx.url, ""); // empty token — file gets written by login

    let stdin_payload = format!("{}\n{}\n", m.username, common::member::MEMBER_PASSWORD);
    common::pic_no_env(&env)
        .args(["login", "--url", &fx.url])
        .write_stdin(stdin_payload)
        .assert()
        .success()
        .stdout(predicate::str::contains(format!(
            "Logged in as {} (member)",
            m.username
        )));

    let creds_path = env.config_dir.path().join("credentials");
    let body = std::fs::read_to_string(&creds_path).expect("credentials file");
    assert!(
        body.contains(&format!("username = \"{}\"", m.username)),
        "creds should carry the canonical username: {body}",
    );
    // The token persisted must be a real session token, not whatever
    // the user typed — a regression where we accidentally saved the
    // password as the token would fail this check.
    assert!(
        !body.contains(common::member::MEMBER_PASSWORD),
        "password leaked into credentials file: {body}",
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn login_with_wrong_password_errors() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let username = common::unique_username("lpwbad");
    let m = common::member::member_user(fx, &username);
    let env = common::custom_env(&fx.url, "");

    let stdin_payload = format!("{}\nwrong-password\n", m.username);
    common::pic_no_env(&env)
        .args(["login", "--url", &fx.url])
        .write_stdin(stdin_payload)
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 401"));

    let creds_path = env.config_dir.path().join("credentials");
    assert!(
        !creds_path.exists(),
        "failed login must not persist credentials"
    );
}

#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn logout_clears_local_credentials() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    // Use a member's token so we don't yank the admin session out from
    // under parallel tests. The local-file cleanup is the same.
    let username = common::unique_username("lout");
    let m = common::member::member_user(fx, &username);
    let env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&env, &m.username);

    let creds_path = env.config_dir.path().join("credentials");
    assert!(creds_path.exists(), "precondition: creds file seeded");

    common::pic_no_env(&env)
        .args(["logout"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Logged out"));
    assert!(
        !creds_path.exists(),
        "credentials file should be removed after logout"
    );
}

/// `pic logout` is meant to be idempotent: running it with no
/// credentials file present is not an error.
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn logout_is_idempotent_when_already_logged_out() {
    let Some(_fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::TestEnv {
        url: String::new(),
        token: String::new(),
        config_dir: tempfile::TempDir::new().unwrap(),
        home: tempfile::TempDir::new().unwrap(),
    };
    common::pic_no_env(&env)
        .args(["logout"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Logged out"));
}

/// Server-side session invalidation: after `pic logout`, a subsequent
/// `pic whoami` driven by the same (now-stale) token must 401.
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn logout_invalidates_server_session() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let username = common::unique_username("lout2");
    let m = common::member::member_user(fx, &username);
    let env = common::custom_env(&fx.url, &m.token);
    common::seed_credentials(&env, &m.username);

    common::pic_no_env(&env).args(["logout"]).assert().success();

    // Replay the member's old token explicitly — pic_no_env reads the
    // (now-deleted) file, so we go back to env-driven mode with the
    // stale bearer.
    let stale = common::custom_env(&fx.url, &m.token);
    common::pic_as(&stale)
        .args(["whoami"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("HTTP 401"));
}

/// Env vars must override the on-disk credentials file globally. Write
/// garbage into the file, set env to the real admin creds, and prove
/// every read-side command (here `whoami`) goes via env.
#[ignore = "needs DATABASE_URL pointing at a running Postgres"]
#[test]
fn env_vars_override_credentials_file() {
    let Some(fx) = common::fixture_or_skip() else {
        return;
    };
    let env = common::custom_env(&fx.url, &fx.admin_token);
    // Garbage in the file: would 401 if used.
    let body = format!(
        "url = \"{}\"\ntoken = \"pic_stale_garbage_token\"\nusername = \"ghost\"\n",
        env.url
    );
    std::fs::write(env.config_dir.path().join("credentials"), body).unwrap();

    common::pic_as(&env)
        .args(["whoami"])
        .assert()
        .success()
        .stdout(predicate::str::contains(fx.admin_username.as_str()));
}
