use crate::review_backend::{
    models::{MenuChoice, PullRequest, ReviewMenuChoice},
    DefaultReviewBackend, DynReviewBackend,
};

use comfy_table::{presets::UTF8_HORIZONTAL_ONLY, Cell, Table};
use thiserror::Error;

pub struct Review {
    backend: DynReviewBackend,
}

impl Default for Review {
    fn default() -> Self {
        Self::new(std::sync::Arc::new(DefaultReviewBackend::new()))
    }
}

#[derive(Debug, Error)]
pub enum ReviewErrors {
    #[error("user chose to exit")]
    UserExit,
}

impl Review {
    fn new(backend: DynReviewBackend) -> Self {
        Self { backend }
    }

    /// Workflow
    /// 1. Fetch list of repos
    /// 2. Present menu
    /// 3. Choose begin quick review
    /// 4. Present pr and use delta to view changes
    /// 5. Approve, open, skip or quit
    /// 6. Repeat from 4
    fn run(&self, review_requested: Option<String>) -> eyre::Result<()> {
        let prs = self.backend.get_prs(review_requested.clone())?;

        let prs_table = Self::generate_prs_table(&prs);
        self.backend.present_prs(prs_table)?;

        match self.backend.present_menu()? {
            MenuChoice::Exit => eyre::bail!(ReviewErrors::UserExit),
            MenuChoice::Begin => match self.review(&prs)? {
                Some(choice) => match choice {
                    MenuChoice::Exit => eyre::bail!(ReviewErrors::UserExit),
                    MenuChoice::List => return self.run(review_requested.clone()),
                    _ => eyre::bail!("invalid choice"),
                },
                None => {}
            },
            MenuChoice::Search => todo!(),
            MenuChoice::List => return self.run(review_requested.clone()),
        }

        Ok(())
    }

    fn generate_prs_table(prs: &[PullRequest]) -> String {
        let mut table = Table::new();
        let table = table
            .load_preset(UTF8_HORIZONTAL_ONLY)
            .set_content_arrangement(comfy_table::ContentArrangement::Dynamic)
            .set_header(vec![
                Cell::new("repo").add_attribute(comfy_table::Attribute::Bold),
                Cell::new("title").add_attribute(comfy_table::Attribute::Bold),
                Cell::new("number").add_attribute(comfy_table::Attribute::Bold),
            ])
            .add_rows(prs.iter().take(20).map(|pr| {
                let pr = pr.clone();
                vec![
                    Cell::new(pr.repository.name).fg(comfy_table::Color::Green),
                    Cell::new(pr.title),
                    Cell::new(pr.number.to_string()),
                ]
            }));

        table.to_string()
    }

    fn review(&self, prs: &Vec<PullRequest>) -> eyre::Result<Option<MenuChoice>> {
        for pr in prs {
            self.backend.clear()?;
            self.backend.present_pr(pr)?;
            self.review_pr(pr)?;
            if let Some(choice) = self.present_pr_menu(pr)? {
                return Ok(Some(choice));
            }
        }

        Ok(None)
    }

    fn review_pr(&self, pr: &PullRequest) -> eyre::Result<()> {
        self.backend.present_diff(pr)?;
        Ok(())
    }

    fn approve(&self, pr: &PullRequest) -> eyre::Result<()> {
        self.backend.approve(pr)?;

        Ok(())
    }

    fn open_browser(&self, pr: &PullRequest) -> eyre::Result<Option<MenuChoice>> {
        self.backend.pr_open_browser(pr)?;

        self.present_pr_menu(pr)
    }

    fn present_pr_menu(&self, pr: &PullRequest) -> eyre::Result<Option<MenuChoice>> {
        self.backend.present_pr(pr)?;

        match self.backend.present_review_menu(pr)? {
            ReviewMenuChoice::Exit => return Ok(Some(MenuChoice::Exit)),
            ReviewMenuChoice::List => return Ok(Some(MenuChoice::List)),
            ReviewMenuChoice::Approve => {
                self.approve(pr)?;
                return self.present_pr_menu(pr);
            }
            ReviewMenuChoice::Open => return self.open_browser(pr),
            ReviewMenuChoice::Skip => {}
            ReviewMenuChoice::Merge => self.merge(pr)?,
            ReviewMenuChoice::ApproveAndMerge => {
                self.approve(pr)?;
                self.merge(pr)?;
            }
        }

        Ok(None)
    }

    fn merge(&self, pr: &PullRequest) -> eyre::Result<()> {
        self.backend.enable_auto_merge(pr);
        Ok(())
    }
}

impl util::Cmd for Review {
    fn cmd() -> eyre::Result<clap::Command> {
        Ok(clap::Command::new("review"))
    }

    fn exec(_: &clap::ArgMatches) -> eyre::Result<()> {
        Self::default().run(Some("lunarway/squad-aura".into()))
    }
}

#[cfg(test)]
mod tests {
    use crate::review_backend::{
        models::{self, Repository},
        MockReviewBackend,
    };

    use super::*;

    use base64::Engine;
    use mockall::predicate::eq;
    use pretty_assertions::assert_eq;

    #[test]
    fn can_fetch_prs() {
        let mut backend = MockReviewBackend::new();
        let prs = vec![
            PullRequest {
                title: "some-title".into(),
                number: 0,
                repository: Repository {
                    name: "some-name".into(),
                },
            },
            PullRequest {
                title: "some-other-title".into(),
                number: 1,
                repository: Repository {
                    name: "some-other-name".into(),
                },
            },
        ];
        let backendprs = prs.clone();
        backend
            .expect_get_prs()
            .with(eq(Some("kjuulh".into())))
            .times(1)
            .returning(move |_| Ok(backendprs.clone()));

        backend
            .expect_present_menu()
            .times(1)
            .returning(|| Ok(models::MenuChoice::Exit));

        backend.expect_present_prs().times(1).returning(|_| Ok(()));

        let review = Review::new(std::sync::Arc::new(backend));
        let res = review.run(Some("kjuulh".into()));

        assert_err::<ReviewErrors, _>(res)
    }

    #[test]
    fn can_generate_table() {
        let prs = vec![
            PullRequest {
                title: "some-title".into(),
                number: 0,
                repository: Repository {
                    name: "some-name".into(),
                },
            },
            PullRequest {
                title: "some-other-title".into(),
                number: 1,
                repository: Repository {
                    name: "some-other-name".into(),
                },
            },
        ];

        let expected_table = "4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSAChtbMW0gcmVwbyAgICAgICAgICAgIBtbMG0gG1sxbSB0aXRsZSAgICAgICAgICAgIBtbMG0gG1sxbSBudW1iZXIgG1swbQrilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZDilZAKG1szODs1OzEwbSBzb21lLW5hbWUgICAgICAgG1szOW0gIHNvbWUtdGl0bGUgICAgICAgICAwICAgICAgCuKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAobWzM4OzU7MTBtIHNvbWUtb3RoZXItbmFtZSAbWzM5bSAgc29tZS1vdGhlci10aXRsZSAgIDEgICAgICAK4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA";
        let output = Review::generate_prs_table(&prs);

        compare_tables(output, expected_table)
    }

    fn compare_tables(actual: String, snapshot: &str) {
        let b64 = base64::engine::general_purpose::STANDARD_NO_PAD;
        let snapshot = snapshot.clone().replace("\n", "").replace(" ", "");
        println!("expected");
        println!(
            "{}",
            std::str::from_utf8(
                b64.decode(&snapshot)
                    .expect("table to be decodeable")
                    .as_slice()
            )
            .expect("to be utf8")
        );

        println!("actual");
        println!("{actual}");

        assert_eq!(b64.encode(actual), snapshot);
    }

    fn assert_err<TExpected, TVal>(res: eyre::Result<TVal>) {
        match res {
            Err(e) => {
                if !e.is::<ReviewErrors>() {
                    panic!("invalid error: {}", e)
                }
            }
            _ => panic!("error not thrown"),
        }
    }
}
