use anyhow::{Context, Result, anyhow};
use serde::Deserialize;

use super::Repositories;
use super::client::RemoteClient;
use super::spec::RemoteSpec;

#[derive(Deserialize)]
struct GitLabUser {
    id: u64,
}

#[derive(Deserialize)]
struct GitLabProject {
    http_url_to_repo: String,
}

pub async fn repositories(spec: &RemoteSpec) -> Result<Repositories> {
    let client = RemoteClient::new(spec)?;
    let base_url = spec.normalized_base_url();
    let api_url = format!("{}/api/v4", base_url);
    let user_id = fetch_user_id(&client, &api_url, &spec.owner).await?;
    let mut repositories = Vec::new();
    let mut page = 1;

    loop {
        let url = format!(
            "{}/users/{}/projects?page={page}&per_page=100&simple=true",
            api_url, user_id
        );
        let projects = client.get_json::<Vec<GitLabProject>>(&url).await?;
        if projects.is_empty() {
            break;
        }

        repositories.extend(projects.into_iter().map(|project| project.http_url_to_repo));
        page += 1;
    }

    Ok(repositories)
}

async fn fetch_user_id(client: &RemoteClient, api_url: &str, owner: &str) -> Result<u64> {
    let url = format!("{api_url}/users?username={owner}");
    let users = client.get_json::<Vec<GitLabUser>>(&url).await?;

    users
        .into_iter()
        .next()
        .map(|user| user.id)
        .ok_or_else(|| anyhow!("GitLab owner '{owner}' was not found"))
        .with_context(|| format!("failed to resolve GitLab owner '{owner}'"))
}

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

    #[tokio::test]
    async fn fetch_user_id_returns_first_matching_user_id() {
        let server = spawn_server(vec![(
            "/api/v4/users?username=wykwit".to_string(),
            StubResponse::json(r#"[{"id":42}]"#),
        )])
        .await;
        let spec = RemoteSpec::gitlab("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let user_id = fetch_user_id(&client, &format!("{}/api/v4", server.url()), "wykwit")
            .await
            .unwrap();

        assert_eq!(user_id, 42);
    }

    #[tokio::test]
    async fn fetch_user_id_errors_when_owner_is_missing() {
        let server = spawn_server(vec![(
            "/api/v4/users?username=wykwit".to_string(),
            StubResponse::json("[]"),
        )])
        .await;
        let spec = RemoteSpec::gitlab("wykwit").unwrap();
        let client = RemoteClient::new(&spec).unwrap();

        let error = fetch_user_id(&client, &format!("{}/api/v4", server.url()), "wykwit")
            .await
            .unwrap_err();

        assert!(
            error
                .to_string()
                .contains("failed to resolve GitLab owner 'wykwit'")
        );
    }

    #[tokio::test]
    async fn repositories_collects_all_pages() {
        let server = spawn_server(vec![
            (
                "/api/v4/users?username=wykwit".to_string(),
                StubResponse::json(r#"[{"id":7}]"#),
            ),
            (
                "/api/v4/users/7/projects?page=1&per_page=100&simple=true".to_string(),
                StubResponse::json(r#"[{"http_url_to_repo":"https://example.com/one.git"}]"#),
            ),
            (
                "/api/v4/users/7/projects?page=2&per_page=100&simple=true".to_string(),
                StubResponse::json(r#"[{"http_url_to_repo":"https://example.com/two.git"}]"#),
            ),
            (
                "/api/v4/users/7/projects?page=3&per_page=100&simple=true".to_string(),
                StubResponse::json("[]"),
            ),
        ])
        .await;
        let spec = RemoteSpec {
            kind: ProviderKind::GitLab,
            base_url: Some(server.url().to_string()),
            owner: "wykwit".to_string(),
        };

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

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