use std::{error::Error, io::Read};

use gotcha_gitea::{
    Client, apis,
    models::{
        ChangedFile, Comment, Commit, CreateIssueCommentOption, CreateIssueOption,
        CreateMilestoneOption, CreatePullRequestOption, EditIssueOption, EditMilestoneOption,
        EditPullRequestOption, Issue, MergePullRequestOption, Milestone, PullRequest, PullReview,
    },
};
use serde::de::DeserializeOwned;

use crate::config::{RepositoryScope, Selection};

const ISSUE_HELP: &str = "\
Usage: gotcha issue SUBCOMMAND

Subcommands:
  list [OWNER/REPOSITORY]          List issues
  show INDEX [OWNER/REPOSITORY]    Show an issue
  create [OWNER/REPOSITORY]        Create from CreateIssueOption YAML on stdin
  edit INDEX [OWNER/REPOSITORY]    Edit from EditIssueOption YAML on stdin
  delete INDEX [OWNER/REPOSITORY]  Delete an issue
  comments INDEX [OWNER/REPOSITORY]
                                  List comments
  comment INDEX [OWNER/REPOSITORY] Add a comment read as text from stdin";

const MILESTONE_HELP: &str = "\
Usage: gotcha milestone SUBCOMMAND

Subcommands:
  list [OWNER/REPOSITORY]       List milestones
  show ID [OWNER/REPOSITORY]    Show a milestone
  create [OWNER/REPOSITORY]     Create from CreateMilestoneOption YAML on stdin
  edit ID [OWNER/REPOSITORY]    Edit from EditMilestoneOption YAML on stdin
  delete ID [OWNER/REPOSITORY]  Delete a milestone";

const PULL_HELP: &str = "\
Usage: gotcha pull SUBCOMMAND

Subcommands:
  list [OWNER/REPOSITORY]          List pull requests
  show INDEX [OWNER/REPOSITORY]    Show a pull request
  create [OWNER/REPOSITORY]        Create from CreatePullRequestOption YAML on stdin
  edit INDEX [OWNER/REPOSITORY]    Edit from EditPullRequestOption YAML on stdin
  merge INDEX [OWNER/REPOSITORY]   Merge from MergePullRequestOption YAML on stdin
  commits INDEX [OWNER/REPOSITORY] List commits
  files INDEX [OWNER/REPOSITORY]   List changed files
  reviews INDEX [OWNER/REPOSITORY] List reviews";

pub fn domain_help(domain: &str) -> Option<&'static str> {
    match domain {
        "issue" => Some(ISSUE_HELP),
        "milestone" => Some(MILESTONE_HELP),
        "pull" => Some(PULL_HELP),
        _ => None,
    }
}

pub fn subcommand_help(domain: &str, command: &str) -> Option<&'static str> {
    match (domain, command) {
        ("issue", "list") => Some("Usage: gotcha issue list [OWNER/REPOSITORY]"),
        ("issue", "show") => Some("Usage: gotcha issue show INDEX [OWNER/REPOSITORY]"),
        ("issue", "create") => Some(
            "Usage: gotcha issue create [OWNER/REPOSITORY] < issue.yaml\n\nReads CreateIssueOption YAML from stdin, for example:\n  title: Fix the bug\n  body: Reproduction steps",
        ),
        ("issue", "edit") => Some(
            "Usage: gotcha issue edit INDEX [OWNER/REPOSITORY] < issue.yaml\n\nReads EditIssueOption YAML from stdin.",
        ),
        ("issue", "delete") => Some("Usage: gotcha issue delete INDEX [OWNER/REPOSITORY]"),
        ("issue", "comments") => Some("Usage: gotcha issue comments INDEX [OWNER/REPOSITORY]"),
        ("issue", "comment") => Some(
            "Usage: gotcha issue comment INDEX [OWNER/REPOSITORY] < comment.txt\n\nReads the comment body as text from stdin.",
        ),
        ("milestone", "list") => Some("Usage: gotcha milestone list [OWNER/REPOSITORY]"),
        ("milestone", "show") => Some("Usage: gotcha milestone show ID [OWNER/REPOSITORY]"),
        ("milestone", "create") => Some(
            "Usage: gotcha milestone create [OWNER/REPOSITORY] < milestone.yaml\n\nReads CreateMilestoneOption YAML from stdin, for example:\n  title: Version 1.0\n  due_on: 2026-09-01T00:00:00Z",
        ),
        ("milestone", "edit") => Some(
            "Usage: gotcha milestone edit ID [OWNER/REPOSITORY] < milestone.yaml\n\nReads EditMilestoneOption YAML from stdin.",
        ),
        ("milestone", "delete") => Some("Usage: gotcha milestone delete ID [OWNER/REPOSITORY]"),
        ("pull", "list") => Some("Usage: gotcha pull list [OWNER/REPOSITORY]"),
        ("pull", "show") => Some("Usage: gotcha pull show INDEX [OWNER/REPOSITORY]"),
        ("pull", "create") => Some(
            "Usage: gotcha pull create [OWNER/REPOSITORY] < pull.yaml\n\nReads CreatePullRequestOption YAML from stdin, for example:\n  title: Add feature\n  head: feature\n  base: main",
        ),
        ("pull", "edit") => Some(
            "Usage: gotcha pull edit INDEX [OWNER/REPOSITORY] < pull.yaml\n\nReads EditPullRequestOption YAML from stdin.",
        ),
        ("pull", "merge") => Some(
            "Usage: gotcha pull merge INDEX [OWNER/REPOSITORY] < merge.yaml\n\nReads MergePullRequestOption YAML from stdin, for example:\n  Do: squash\n  delete_branch_after_merge: true",
        ),
        ("pull", "commits") => Some("Usage: gotcha pull commits INDEX [OWNER/REPOSITORY]"),
        ("pull", "files") => Some("Usage: gotcha pull files INDEX [OWNER/REPOSITORY]"),
        ("pull", "reviews") => Some("Usage: gotcha pull reviews INDEX [OWNER/REPOSITORY]"),
        _ => None,
    }
}

pub async fn run(
    command: &[String],
    selection: &Selection,
    client: &Client,
) -> Result<bool, Box<dyn Error>> {
    let configuration = client.configuration();
    match command {
        [domain, action] if domain == "issue" && action == "list" => {
            let scope = scope(selection, None)?;
            let issues = apis::issue_api::issue_list_issues(
                &configuration,
                &scope.owner,
                &scope.repository,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
            )
            .await?;
            print_issues(&issues);
        }
        [domain, action, repository] if domain == "issue" && action == "list" => {
            let scope = scope(selection, Some(repository))?;
            let issues = apis::issue_api::issue_list_issues(
                &configuration,
                &scope.owner,
                &scope.repository,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
                None,
            )
            .await?;
            print_issues(&issues);
        }
        [domain, action, index] if domain == "issue" && action == "show" => {
            let scope = scope(selection, None)?;
            print_issue(
                &apis::issue_api::issue_get_issue(
                    &configuration,
                    &scope.owner,
                    &scope.repository,
                    number(index)?,
                )
                .await?,
            );
        }
        [domain, action, index, repository] if domain == "issue" && action == "show" => {
            let scope = scope(selection, Some(repository))?;
            print_issue(
                &apis::issue_api::issue_get_issue(
                    &configuration,
                    &scope.owner,
                    &scope.repository,
                    number(index)?,
                )
                .await?,
            );
        }
        [domain, action] if domain == "issue" && action == "create" => {
            create_issue(&configuration, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "issue" && action == "create" => {
            create_issue(&configuration, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, index] if domain == "issue" && action == "edit" => {
            edit_issue(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "edit" => {
            edit_issue(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action, index] if domain == "issue" && action == "delete" => {
            delete_issue(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "delete" => {
            delete_issue(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action, index] if domain == "issue" && action == "comments" => {
            list_comments(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "comments" => {
            list_comments(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action, index] if domain == "issue" && action == "comment" => {
            create_comment(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "issue" && action == "comment" => {
            create_comment(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action] if domain == "milestone" && action == "list" => {
            list_milestones(&configuration, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "milestone" && action == "list" => {
            list_milestones(&configuration, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "show" => {
            show_milestone(&configuration, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "show" => {
            show_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action] if domain == "milestone" && action == "create" => {
            create_milestone(&configuration, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "milestone" && action == "create" => {
            create_milestone(&configuration, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "edit" => {
            edit_milestone(&configuration, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "edit" => {
            edit_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action, id] if domain == "milestone" && action == "delete" => {
            delete_milestone(&configuration, &scope(selection, None)?, id).await?;
        }
        [domain, action, id, repository] if domain == "milestone" && action == "delete" => {
            delete_milestone(&configuration, &scope(selection, Some(repository))?, id).await?;
        }
        [domain, action] if domain == "pull" && action == "list" => {
            list_pulls(&configuration, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "pull" && action == "list" => {
            list_pulls(&configuration, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, index] if domain == "pull" && action == "show" => {
            show_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "show" => {
            show_pull(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action] if domain == "pull" && action == "create" => {
            create_pull(&configuration, &scope(selection, None)?).await?;
        }
        [domain, action, repository] if domain == "pull" && action == "create" => {
            create_pull(&configuration, &scope(selection, Some(repository))?).await?;
        }
        [domain, action, index] if domain == "pull" && action == "edit" => {
            edit_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "edit" => {
            edit_pull(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action, index] if domain == "pull" && action == "merge" => {
            merge_pull(&configuration, &scope(selection, None)?, number(index)?).await?;
        }
        [domain, action, index, repository] if domain == "pull" && action == "merge" => {
            merge_pull(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
            )
            .await?;
        }
        [domain, action, index]
            if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
        {
            pull_details(
                &configuration,
                &scope(selection, None)?,
                number(index)?,
                action,
            )
            .await?;
        }
        [domain, action, index, repository]
            if domain == "pull" && matches!(action.as_str(), "commits" | "files" | "reviews") =>
        {
            pull_details(
                &configuration,
                &scope(selection, Some(repository))?,
                number(index)?,
                action,
            )
            .await?;
        }
        [domain, ..] if matches!(domain.as_str(), "issue" | "milestone" | "pull") => {
            return Ok(false);
        }
        _ => return Ok(false),
    }
    Ok(true)
}

fn scope(selection: &Selection, value: Option<&str>) -> Result<RepositoryScope, Box<dyn Error>> {
    match value {
        Some(value) => Ok(RepositoryScope::parse(value)?),
        None => selection.repository.clone().ok_or_else(|| {
            "cannot infer a repository; run inside one or pass OWNER/REPOSITORY".into()
        }),
    }
}

fn number(value: &str) -> Result<i64, Box<dyn Error>> {
    let number: i64 = value.parse()?;
    if number < 1 {
        return Err("index must be a positive integer".into());
    }
    Ok(number)
}

fn read_stdin() -> Result<String, Box<dyn Error>> {
    let mut input = String::new();
    std::io::stdin().read_to_string(&mut input)?;
    if input.trim().is_empty() {
        return Err("stdin must not be empty".into());
    }
    Ok(input)
}

fn read_yaml<T: DeserializeOwned>() -> Result<T, Box<dyn Error>> {
    parse_yaml(&read_stdin()?)
}

fn parse_yaml<T: DeserializeOwned>(input: &str) -> Result<T, Box<dyn Error>> {
    Ok(serde_yaml::from_str(input)?)
}

async fn create_issue(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
    let body: CreateIssueOption = read_yaml()?;
    if body.title.trim().is_empty() {
        return Err("issue title must not be empty".into());
    }
    print_issue(
        &apis::issue_api::issue_create_issue(
            configuration,
            &scope.owner,
            &scope.repository,
            Some(body),
        )
        .await?,
    );
    Ok(())
}

async fn edit_issue(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let issue = apis::issue_api::issue_edit_issue(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
        Some(read_yaml::<EditIssueOption>()?),
    )
    .await?;
    print_issue(&issue);
    Ok(())
}

async fn delete_issue(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    apis::issue_api::issue_delete(configuration, &scope.owner, &scope.repository, index).await?;
    println!("Deleted issue #{index}.");
    Ok(())
}

async fn list_comments(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let comments = apis::issue_api::issue_get_comments(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
        None,
        None,
    )
    .await?;
    print_comments(&comments);
    Ok(())
}

async fn create_comment(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let body = read_stdin()?.trim_end().to_owned();
    let comment = apis::issue_api::issue_create_comment(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
        Some(CreateIssueCommentOption::new(body)),
    )
    .await?;
    print_comments(&[comment]);
    Ok(())
}

async fn list_milestones(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
    let milestones = apis::issue_api::issue_get_milestones_list(
        configuration,
        &scope.owner,
        &scope.repository,
        None,
        None,
        None,
        None,
    )
    .await?;
    print_milestones(&milestones);
    Ok(())
}

async fn show_milestone(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    let milestone =
        apis::issue_api::issue_get_milestone(configuration, &scope.owner, &scope.repository, id)
            .await?;
    print_milestone(&milestone);
    Ok(())
}

async fn create_milestone(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
    let body: CreateMilestoneOption = read_yaml()?;
    if body.title.as_deref().unwrap_or_default().trim().is_empty() {
        return Err("milestone title must not be empty".into());
    }
    let milestone = apis::issue_api::issue_create_milestone(
        configuration,
        &scope.owner,
        &scope.repository,
        Some(body),
    )
    .await?;
    print_milestone(&milestone);
    Ok(())
}

async fn edit_milestone(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    let milestone = apis::issue_api::issue_edit_milestone(
        configuration,
        &scope.owner,
        &scope.repository,
        id,
        Some(read_yaml::<EditMilestoneOption>()?),
    )
    .await?;
    print_milestone(&milestone);
    Ok(())
}

async fn delete_milestone(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    id: &str,
) -> Result<(), Box<dyn Error>> {
    apis::issue_api::issue_delete_milestone(configuration, &scope.owner, &scope.repository, id)
        .await?;
    println!("Deleted milestone {id}.");
    Ok(())
}

async fn list_pulls(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
    let pulls = apis::repository_api::repo_list_pull_requests(
        configuration,
        &scope.owner,
        &scope.repository,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
    )
    .await?;
    print_pulls(&pulls);
    Ok(())
}

async fn show_pull(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let pull = apis::repository_api::repo_get_pull_request(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
    )
    .await?;
    print_pull(&pull);
    Ok(())
}

async fn create_pull(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
) -> Result<(), Box<dyn Error>> {
    let body: CreatePullRequestOption = read_yaml()?;
    if [
        body.title.as_deref(),
        body.head.as_deref(),
        body.base.as_deref(),
    ]
    .into_iter()
    .any(|value| value.unwrap_or_default().trim().is_empty())
    {
        return Err("pull request title, head, and base must not be empty".into());
    }
    let pull = apis::repository_api::repo_create_pull_request(
        configuration,
        &scope.owner,
        &scope.repository,
        Some(body),
    )
    .await?;
    print_pull(&pull);
    Ok(())
}

async fn edit_pull(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    let pull = apis::repository_api::repo_edit_pull_request(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
        Some(read_yaml::<EditPullRequestOption>()?),
    )
    .await?;
    print_pull(&pull);
    Ok(())
}

async fn merge_pull(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
) -> Result<(), Box<dyn Error>> {
    apis::repository_api::repo_merge_pull_request(
        configuration,
        &scope.owner,
        &scope.repository,
        index,
        Some(read_yaml::<MergePullRequestOption>()?),
    )
    .await?;
    println!("Merged pull request #{index}.");
    Ok(())
}

async fn pull_details(
    configuration: &apis::configuration::Configuration,
    scope: &RepositoryScope,
    index: i64,
    action: &str,
) -> Result<(), Box<dyn Error>> {
    match action {
        "commits" => print_commits(
            &apis::repository_api::repo_get_pull_request_commits(
                configuration,
                &scope.owner,
                &scope.repository,
                index,
                None,
                None,
                None,
                None,
            )
            .await?,
        ),
        "files" => print_files(
            &apis::repository_api::repo_get_pull_request_files(
                configuration,
                &scope.owner,
                &scope.repository,
                index,
                None,
                None,
                None,
                None,
            )
            .await?,
        ),
        "reviews" => print_reviews(
            &apis::repository_api::repo_list_pull_reviews(
                configuration,
                &scope.owner,
                &scope.repository,
                index,
                None,
                None,
            )
            .await?,
        ),
        _ => unreachable!(),
    }
    Ok(())
}

fn print_issues(issues: &[Issue]) {
    println!("INDEX\tSTATE\tTITLE\tUPDATED");
    for issue in issues {
        println!(
            "{}\t{}\t{}\t{}",
            issue.number.unwrap_or_default(),
            issue.state.as_deref().unwrap_or("unknown"),
            one_line(issue.title.as_deref().unwrap_or("")),
            issue.updated_at.as_deref().unwrap_or("-")
        );
    }
}

fn print_issue(issue: &Issue) {
    println!(
        "#{} [{}] {}",
        issue.number.unwrap_or_default(),
        issue.state.as_deref().unwrap_or("unknown"),
        issue.title.as_deref().unwrap_or("")
    );
    field("url", issue.html_url.as_deref());
    field(
        "author",
        issue.user.as_deref().and_then(|user| user.login.as_deref()),
    );
    field("updated", issue.updated_at.as_deref());
    body(issue.body.as_deref());
}

fn print_comments(comments: &[Comment]) {
    for comment in comments {
        println!(
            "{} · {}",
            comment
                .user
                .as_deref()
                .and_then(|user| user.login.as_deref())
                .unwrap_or("unknown"),
            comment.created_at.as_deref().unwrap_or("-")
        );
        body(comment.body.as_deref());
    }
}

fn print_milestones(milestones: &[Milestone]) {
    println!("ID\tSTATE\tTITLE\tDUE\tOPEN/CLOSED");
    for milestone in milestones {
        println!(
            "{}\t{}\t{}\t{}\t{}/{}",
            milestone.id.unwrap_or_default(),
            milestone.state.as_deref().unwrap_or("unknown"),
            one_line(milestone.title.as_deref().unwrap_or("")),
            milestone.due_on.as_deref().unwrap_or("-"),
            milestone.open_issues.unwrap_or_default(),
            milestone.closed_issues.unwrap_or_default()
        );
    }
}

fn print_milestone(milestone: &Milestone) {
    println!(
        "{} [{}] {}",
        milestone.id.unwrap_or_default(),
        milestone.state.as_deref().unwrap_or("unknown"),
        milestone.title.as_deref().unwrap_or("")
    );
    field("due", milestone.due_on.as_deref());
    println!(
        "issues: {} open, {} closed",
        milestone.open_issues.unwrap_or_default(),
        milestone.closed_issues.unwrap_or_default()
    );
    body(milestone.description.as_deref());
}

fn print_pulls(pulls: &[PullRequest]) {
    println!("INDEX\tSTATE\tTITLE\tUPDATED");
    for pull in pulls {
        println!(
            "{}\t{}\t{}\t{}",
            pull.number.unwrap_or_default(),
            pull.state.as_deref().unwrap_or("unknown"),
            one_line(pull.title.as_deref().unwrap_or("")),
            pull.updated_at.as_deref().unwrap_or("-")
        );
    }
}

fn print_pull(pull: &PullRequest) {
    println!(
        "#{} [{}] {}",
        pull.number.unwrap_or_default(),
        pull.state.as_deref().unwrap_or("unknown"),
        pull.title.as_deref().unwrap_or("")
    );
    field("url", pull.html_url.as_deref());
    field(
        "author",
        pull.user.as_deref().and_then(|user| user.login.as_deref()),
    );
    field("updated", pull.updated_at.as_deref());
    println!("mergeable: {}", pull.mergeable.unwrap_or(false));
    body(pull.body.as_deref());
}

fn print_commits(commits: &[Commit]) {
    println!("SHA\tMESSAGE");
    for commit in commits {
        println!(
            "{}\t{}",
            commit.sha.as_deref().unwrap_or("unknown"),
            one_line(
                commit
                    .commit
                    .as_deref()
                    .and_then(|commit| commit.message.as_deref())
                    .unwrap_or("")
            )
        );
    }
}

fn print_files(files: &[ChangedFile]) {
    println!("STATUS\tCHANGES\tFILE");
    for file in files {
        println!(
            "{}\t{}\t{}",
            file.status.as_deref().unwrap_or("unknown"),
            file.changes.unwrap_or_default(),
            file.filename.as_deref().unwrap_or("")
        );
    }
}

fn print_reviews(reviews: &[PullReview]) {
    println!("ID\tSTATE\tREVIEWER\tSUBMITTED");
    for review in reviews {
        println!(
            "{}\t{}\t{}\t{}",
            review.id.unwrap_or_default(),
            review.state.as_deref().unwrap_or("unknown"),
            review
                .user
                .as_deref()
                .and_then(|user| user.login.as_deref())
                .unwrap_or("unknown"),
            review.submitted_at.as_deref().unwrap_or("-")
        );
    }
}

fn field(name: &str, value: Option<&str>) {
    if let Some(value) = value.filter(|value| !value.is_empty()) {
        println!("{name}: {value}");
    }
}

fn body(value: Option<&str>) {
    if let Some(value) = value.filter(|value| !value.is_empty()) {
        println!("\n{value}");
    }
}

fn one_line(value: &str) -> &str {
    value.lines().next().unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_typed_yaml_and_rejects_bad_indexes() {
        let issue: CreateIssueOption = parse_yaml("title: Fix it\nbody: Details\n").unwrap();

        assert_eq!(issue.title, "Fix it");
        assert_eq!(issue.body.as_deref(), Some("Details"));
        assert!(number("0").is_err());
        assert!(number("7").is_ok());
    }
}
