/* git/test_helpers.rs
 *
 * Copyright 2026 Christian Kruse
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

use std::fs;
use std::path::{Path, PathBuf};

use git2::{Oid, Repository, Signature};
use tempfile::TempDir;

pub(super) fn bare_repo() -> (TempDir, PathBuf) {
    let tmp = TempDir::new().expect("create tempdir");
    let path = tmp.path().to_path_buf();

    Repository::init_bare(&path).expect("init bare repo");

    (tmp, path)
}

pub(super) struct TestRepo {
    pub _tmp: TempDir,
    pub path: PathBuf,
    pub repo: Repository,
    pub main_branch: String,
}

impl TestRepo {
    pub(super) fn new() -> Self {
        let tmp = TempDir::new().expect("create tempdir");
        let path = tmp.path().to_path_buf();

        let mut opts = git2::RepositoryInitOptions::new();
        opts.initial_head("main");
        let repo = Repository::init_opts(&path, &opts).expect("init repo");

        let mut cfg = repo.config().expect("repo config");
        cfg.set_str("user.name", "Tester").unwrap();
        cfg.set_str("user.email", "tester@example.invalid").unwrap();

        Self {
            _tmp: tmp,
            path,
            repo,
            main_branch: "main".to_string(),
        }
    }

    pub(super) fn sig(&self) -> Signature<'static> {
        Signature::now("Tester", "tester@example.invalid").unwrap()
    }

    pub(super) fn write_file(&self, file: &str, content: &str) {
        let p = self.path.join(file);

        if let Some(parent) = p.parent() {
            fs::create_dir_all(parent).unwrap();
        }

        fs::write(p, content).unwrap();
    }

    pub(super) fn commit(&self, message: &str, file: &str, content: &str) -> Oid {
        self.write_file(file, content);

        let mut index = self.repo.index().unwrap();
        index.add_path(Path::new(file)).unwrap();
        index.write().unwrap();

        let tree_oid = index.write_tree().unwrap();
        let tree = self.repo.find_tree(tree_oid).unwrap();
        let sig = self.sig();

        let parents: Vec<git2::Commit> = match self.repo.head() {
            Ok(head) => vec![head.peel_to_commit().unwrap()],
            Err(_) => vec![],
        };

        let parent_refs: Vec<&git2::Commit> = parents.iter().collect();

        self.repo
            .commit(Some("HEAD"), &sig, &sig, message, &tree, &parent_refs)
            .unwrap()
    }

    pub(super) fn merge_commit(&self, message: &str, parents: &[Oid]) -> Oid {
        let parent_commits: Vec<git2::Commit> = parents.iter().map(|o| self.repo.find_commit(*o).unwrap()).collect();
        let parent_refs: Vec<&git2::Commit> = parent_commits.iter().collect();
        let tree = parent_commits[0].tree().unwrap();
        let sig = self.sig();

        self.repo
            .commit(Some("HEAD"), &sig, &sig, message, &tree, &parent_refs)
            .unwrap()
    }

    pub(super) fn head_oid(&self) -> Oid {
        self.repo.head().unwrap().peel_to_commit().unwrap().id()
    }

    pub(super) fn first_parent_chain_len(&self) -> usize {
        let mut count = 0usize;
        let mut current = self.repo.head().unwrap().peel_to_commit().unwrap();

        loop {
            count += 1;

            if current.parent_count() == 0 {
                break;
            }

            current = current.parent(0).unwrap();
        }

        count
    }

    pub(super) fn read_head_blob(&self, file: &str) -> String {
        let head_tree = self.repo.head().unwrap().peel_to_commit().unwrap().tree().unwrap();
        let entry = head_tree.get_path(Path::new(file)).unwrap();
        let blob = self.repo.find_blob(entry.id()).unwrap();

        String::from_utf8(blob.content().to_vec()).unwrap()
    }

    pub(super) fn head_message(&self) -> String {
        self.repo
            .head()
            .unwrap()
            .peel_to_commit()
            .unwrap()
            .message()
            .unwrap_or("")
            .to_string()
    }

    pub(super) fn head_tree_oid(&self) -> Oid {
        self.repo.head().unwrap().peel_to_commit().unwrap().tree().unwrap().id()
    }

    pub(super) fn set_head_to_branch(&self, name: &str) {
        self.repo.set_head(&format!("refs/heads/{name}")).unwrap();

        self.repo
            .checkout_head(Some(git2::build::CheckoutBuilder::new().force()))
            .unwrap();
    }

    pub(super) fn create_branch_at(&self, name: &str, oid: Oid) {
        let commit = self.repo.find_commit(oid).unwrap();
        self.repo.branch(name, &commit, false).unwrap();
    }

    pub(super) fn fresh_index(&self) -> git2::Index {
        let mut idx = self.repo.index().unwrap();
        idx.read(true).unwrap();

        idx
    }
}

pub(super) fn s(oid: Oid) -> String {
    oid.to_string()
}

pub(super) fn gc() -> Vec<String> {
    vec!["git".to_string()]
}
