use anyhow::Result;
use serde::Deserialize;

use super::Repositories;
use super::client::{RemoteClient, parse_next_link};
use super::spec::RemoteSpec;

#[derive(Deserialize)]
struct GitHubRepo {
    clone_url: String,
    owner: GitHubOwner,
}

#[derive(Deserialize)]
struct GitHubOwner {
    login: String,
}

#[derive(Deserialize)]
struct GitHubViewer {
    login: String,
}

pub async fn repositories(spec: &RemoteSpec) -> Result<Repositories> {
    let client = RemoteClient::new(spec)?;
    let base_url = spec.normalized_base_url();
    let mut repositories = Vec::new();
    let mut next_url = Some(initial_url(&client, &base_url, spec).await?);

    while let Some(url) = next_url.take() {
        let (page, headers) = client
            .get_json_with_headers::<Vec<GitHubRepo>>(&url)
            .await?;
        repositories.extend(
            page.into_iter()
                .filter(|repo| repo.owner.login.eq_ignore_ascii_case(&spec.owner))
                .map(|repo| repo.clone_url),
        );
        next_url = parse_next_link(&headers)?;
    }

    Ok(repositories)
}

async fn initial_url(client: &RemoteClient, base_url: &str, spec: &RemoteSpec) -> Result<String> {
    if !spec.has_auth_token() {
        return Ok(format!(
            "{base_url}/users/{}/repos?per_page=100",
            spec.owner
        ));
    }

    let viewer = client
        .get_json::<GitHubViewer>(&format!("{base_url}/user"))
        .await?;

    if viewer.login.eq_ignore_ascii_case(&spec.owner) {
        return Ok(format!(
            "{base_url}/user/repos?per_page=100&visibility=all&affiliation=owner,collaborator,organization_member"
        ));
    }

    let org_url = format!("{base_url}/orgs/{}/repos?per_page=100&type=all", spec.owner);
    if client
        .get_json_optional::<Vec<GitHubRepo>>(&org_url)
        .await?
        .is_some()
    {
        return Ok(org_url);
    }

    Ok(format!(
        "{base_url}/users/{}/repos?per_page=100",
        spec.owner
    ))
}

#[cfg(test)]
mod tests {
    use super::{initial_url, repositories};
    use crate::remotes::RemoteSpec;
    use crate::remotes::client::RemoteClient;
    use crate::testing::{StubResponse, env_lock, remove_env_var, set_env_var, spawn_server};

    #[tokio::test]
    async fn initial_url_uses_public_user_endpoint_without_auth() {
        let _guard = env_lock().lock().unwrap();
        remove_env_var("GITHUB_TOKEN");
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();
        let base_url = "https://api.github.com";

        let url = initial_url(&client, base_url, &spec).await.unwrap();

        assert_eq!(
            url,
            "https://api.github.com/users/wykwit/repos?per_page=100"
        );
    }

    #[tokio::test]
    async fn initial_url_uses_authenticated_user_endpoint_for_matching_viewer() {
        let _guard = env_lock().lock().unwrap();
        set_env_var("GITHUB_TOKEN", "token");

        let server = spawn_server(vec![(
            "/user".to_string(),
            StubResponse::json(r#"{"login":"wykwit"}"#),
        )])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let url = initial_url(&client, server.url(), &spec).await.unwrap();

        assert_eq!(
            url,
            format!(
                "{}/user/repos?per_page=100&visibility=all&affiliation=owner,collaborator,organization_member",
                server.url()
            )
        );

        remove_env_var("GITHUB_TOKEN");
    }

    #[tokio::test]
    async fn initial_url_uses_org_endpoint_when_owner_is_an_organization() {
        let _guard = env_lock().lock().unwrap();
        set_env_var("GITHUB_TOKEN", "token");

        let server = spawn_server(vec![
            (
                "/user".to_string(),
                StubResponse::json(r#"{"login":"another-user"}"#),
            ),
            (
                "/orgs/wykwit/repos?per_page=100&type=all".to_string(),
                StubResponse::json("[]"),
            ),
        ])
        .await;
        let spec = RemoteSpec::github("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let url = initial_url(&client, server.url(), &spec).await.unwrap();

        assert_eq!(
            url,
            format!("{}/orgs/wykwit/repos?per_page=100&type=all", server.url())
        );

        remove_env_var("GITHUB_TOKEN");
    }

    #[tokio::test]
    async fn repositories_filters_results_and_follows_pagination() {
        let _guard = env_lock().lock().unwrap();
        remove_env_var("GITHUB_TOKEN");
        let server = spawn_server(vec![
            (
                "/users/wykwit/repos?per_page=100".to_string(),
                StubResponse::json(
                    r#"[
                        {"clone_url":"https://example.com/one.git","owner":{"login":"wykwit"}},
                        {"clone_url":"https://example.com/two.git","owner":{"login":"someone-else"}}
                    ]"#,
                )
                .with_header("link", r#"<{{base_url}}/next?page=2>; rel="next""#),
            ),
            (
                "/next?page=2".to_string(),
                StubResponse::json(
                    r#"[{"clone_url":"https://example.com/three.git","owner":{"login":"WyKwIt"}}]"#,
                ),
            ),
        ])
        .await;
        let spec = RemoteSpec {
            kind: crate::remotes::ProviderKind::GitHub,
            base_url: Some(server.url().to_string()),
            owner: "wykwit".to_string(),
        };

        let repositories = repositories(&spec).await.unwrap();

        assert_eq!(
            repositories,
            vec![
                "https://example.com/one.git".to_string(),
                "https://example.com/three.git".to_string()
            ]
        );
    }
}
