use reqwest::header::ACCEPT;

use crate::{Client, Error, Method, RepositoryId, Result, models, positive};
use gitea_openapi::apis;

#[derive(Debug)]
pub struct ActionRunDetails {
    pub run: models::ActionWorkflowRun,
    pub jobs: Vec<models::ActionWorkflowJob>,
}

#[derive(Debug)]
pub struct ActionJobLog {
    pub job: models::ActionWorkflowJob,
    pub text: String,
}

impl Client {
    pub async fn action_workflows(
        &self,
        repository: &RepositoryId,
    ) -> Result<Vec<models::ActionWorkflow>> {
        apis::repository_api::actions_list_repository_workflows(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
        )
        .await
        .map(|response| response.workflows.unwrap_or_default())
        .map_err(Error::generated)
    }

    pub async fn dispatch_action_workflow(
        &self,
        repository: &RepositoryId,
        workflow: &str,
        reference: &str,
    ) -> Result<()> {
        if workflow.is_empty() || reference.is_empty() {
            return Err(Error::InvalidInput(
                "workflow and reference must not be empty".into(),
            ));
        }
        apis::repository_api::actions_dispatch_workflow(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            workflow,
            Some(models::CreateActionWorkflowDispatch::new(reference.into())),
        )
        .await
        .map_err(Error::generated)
    }

    pub async fn action_runs(
        &self,
        repository: &RepositoryId,
    ) -> Result<Vec<models::ActionWorkflowRun>> {
        apis::repository_api::get_workflow_runs(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            None,
            None,
            None,
            None,
            None,
            Some(1),
            Some(100),
        )
        .await
        .map(|response| response.workflow_runs.unwrap_or_default())
        .map_err(Error::generated)
    }

    pub async fn action_run_details(
        &self,
        repository: &RepositoryId,
        run: i64,
    ) -> Result<ActionRunDetails> {
        positive(run, "run ID")?;
        let run_id = action_run_id(run)?;
        let (run, jobs) = tokio::try_join!(
            async {
                apis::repository_api::get_workflow_run(
                    &self.configuration(),
                    &repository.owner,
                    &repository.repository,
                    &run.to_string(),
                )
                .await
                .map_err(Error::generated)
            },
            async {
                apis::repository_api::list_workflow_run_jobs(
                    &self.configuration(),
                    &repository.owner,
                    &repository.repository,
                    run_id,
                    None,
                    Some(1),
                    Some(100),
                )
                .await
                .map_err(Error::generated)
            }
        )?;
        Ok(ActionRunDetails {
            run,
            jobs: jobs.jobs.unwrap_or_default(),
        })
    }

    pub async fn action_run_logs(
        &self,
        repository: &RepositoryId,
        run: i64,
    ) -> Result<Vec<ActionJobLog>> {
        let run = action_run_id(run)?;
        let jobs = apis::repository_api::list_workflow_run_jobs(
            &self.configuration(),
            &repository.owner,
            &repository.repository,
            run,
            None,
            Some(1),
            Some(100),
        )
        .await
        .map_err(Error::generated)?
        .jobs
        .unwrap_or_default();
        let mut logs = Vec::with_capacity(jobs.len());
        for job in jobs {
            let id = job
                .id
                .ok_or_else(|| Error::Generated("Action job has no ID.".into()))?;
            let endpoint = format!(
                "repos/{}/{}/actions/jobs/{id}/logs",
                apis::urlencode(&repository.owner),
                apis::urlencode(&repository.repository),
            );
            let text = self
                .execute(
                    self.request(Method::GET, &endpoint)?
                        .header(ACCEPT, "text/plain"),
                )
                .await?
                .text()
                .await?;
            logs.push(ActionJobLog { job, text });
        }
        Ok(logs)
    }
}

fn action_run_id(run: i64) -> Result<i32> {
    positive(run, "run ID")?;
    i32::try_from(run).map_err(|_| Error::InvalidInput("run ID is too large".into()))
}

#[cfg(test)]
mod tests {
    use std::{
        io::{Read, Write},
        net::TcpListener,
        thread,
    };

    use super::*;
    use crate::Provider;

    #[tokio::test]
    async fn gets_every_job_log_for_a_run() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let address = listener.local_addr().unwrap();
        let server = thread::spawn(move || {
            let mut requests = Vec::new();
            for (content_type, body) in [
                (
                    "application/json",
                    r#"{"total_count":1,"jobs":[{"id":139,"name":"audit"}]}"#,
                ),
                ("text/plain", "scanner output\nexit code 1\n"),
            ] {
                let (mut stream, _) = listener.accept().unwrap();
                let mut buffer = [0; 4096];
                let length = stream.read(&mut buffer).unwrap();
                requests.push(String::from_utf8_lossy(&buffer[..length]).into_owned());
                write!(
                    stream,
                    "HTTP/1.1 200 OK\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
                    body.len()
                )
                .unwrap();
            }
            requests
        });

        let client =
            Client::with_provider(&format!("http://{address}"), None, Provider::Gitea).unwrap();
        let repository = RepositoryId::new("hugo", "Gotcha").unwrap();
        let logs = client.action_run_logs(&repository, 95).await.unwrap();

        assert_eq!(logs.len(), 1);
        assert_eq!(logs[0].job.name.as_deref(), Some("audit"));
        assert_eq!(logs[0].text, "scanner output\nexit code 1\n");
        let requests = server.join().unwrap();
        assert!(requests[0].starts_with("GET /api/v1/repos/hugo/Gotcha/actions/runs/95/jobs?"));
        assert!(requests[1].starts_with("GET /api/v1/repos/hugo/Gotcha/actions/jobs/139/logs "));
        assert!(requests[1].contains("accept: text/plain"));
    }
}
