use bstr::ByteSlice;
use gix_attributes::search::Outcome;
use gix_worktree::stack::state;

use crate::worktree::stack::probe_case;

#[test]
fn baseline() -> crate::Result {
    // Due to the way our setup differs from gits dynamic stack (which involves trying to read files from disk
    // by path) we can only test one case baseline, so we require multiple platforms (or filesystems) to run this.
    let case = probe_case()?;
    let dir = gix_testtools::scripted_fixture_read_only_standalone("make_attributes_baseline.sh")?;
    let base = dir.join("basics");
    let git_dir = base.join(".git");

    let mut buf = Vec::new();
    let mut collection = gix_attributes::search::MetadataCollection::default();
    let state = gix_worktree::stack::State::for_checkout(
        false,
        gix_worktree::validate::path::component::Options {
            protect_windows: false,
            protect_ntfs: false,
            ..Default::default()
        },
        state::Attributes::new(
            gix_attributes::Search::new_globals([base.join("user.attributes")], &mut buf, &mut collection)?,
            Some(git_dir.join("info").join("attributes")),
            gix_worktree::stack::state::attributes::Source::WorktreeThenIdMapping,
            collection,
        ),
    );

    let mut cache = gix_worktree::Stack::new(&base, state, case, buf, vec![]);

    let mut actual = cache.attribute_matches();
    let input = std::fs::read(base.join("baseline"))?;
    for (rela_path, expected) in (baseline::Expectations { lines: input.lines() }) {
        let entry = cache.at_entry(rela_path, None, &gix_object::find::Never)?;
        let has_match = entry.matching_attributes(&mut actual);

        assert_eq!(
            has_match,
            !expected.is_empty(),
            "matches are reported when git reports them, too"
        );
        assert_references(&actual);
        assert_eq!(actual.iter_selected().count(), 0, "no selection made yet");
        let actual: Vec<_> = actual
            .iter()
            .filter_map(|m| (!m.assignment.state.is_unspecified()).then_some(m.assignment))
            .collect();
        assert_eq!(actual, expected, "we have the same matches: {rela_path:?}");
        assert_eq!(has_match, !actual.is_empty());
    }

    let mut actual = cache.selected_attribute_matches(["info", "test"]);
    let input = std::fs::read(base.join("baseline.selected"))?;
    for (rela_path, expected) in (baseline::Expectations { lines: input.lines() }) {
        let entry = cache.at_entry(rela_path, None, &gix_object::find::Never)?;
        let has_match = entry.matching_attributes(&mut actual);

        assert_eq!(
            has_match,
            !expected.is_empty(),
            "matches are reported when git reports them, too"
        );
        assert_references(&actual);
        let actual: Vec<_> = actual.iter_selected().map(|m| m.assignment).collect();
        assert_eq!(actual, expected, "we have the same matches: {rela_path:?}");
        assert_eq!(has_match, !actual.is_empty());
    }

    Ok(())
}

fn assert_references(out: &Outcome) {
    for m in out.iter() {
        if let Some(source) = m.kind.source_id() {
            let sm = out
                .match_by_id(source)
                .expect("sources are always available in the outcome");
            assert_ne!(
                sm.assignment.name, m.assignment.name,
                "it's impossible to resolve to ourselves"
            );
        }
    }
}

mod baseline {
    use bstr::{BStr, ByteSlice};
    use gix_attributes::{AssignmentRef, StateRef};

    pub struct Expectations<'a> {
        pub lines: bstr::Lines<'a>,
    }

    impl<'a> Iterator for Expectations<'a> {
        type Item = (
            &'a BStr,
            // Names might refer to attributes or macros
            Vec<AssignmentRef<'a>>,
        );

        fn next(&mut self) -> Option<Self::Item> {
            let path = self.lines.next()?;
            let mut assignments = Vec::new();
            loop {
                let line = self.lines.next()?;
                if line.is_empty() {
                    return Some((path.as_bstr(), assignments));
                }

                let mut prev = None;
                let mut tokens = line.splitn(3, |b| {
                    let is_match = *b == b' ' && prev.take() == Some(b':');
                    prev = Some(*b);
                    is_match
                });

                if let Some(((_path, attr), info)) = tokens.next().zip(tokens.next()).zip(tokens.next()) {
                    let state = match info {
                        b"set" => StateRef::Set,
                        b"unset" => StateRef::Unset,
                        b"unspecified" => StateRef::Unspecified,
                        _ => StateRef::from_bytes(info),
                    };
                    let attr = attr.trim_end_with(|b| b == ':');
                    assignments.push(AssignmentRef {
                        name: gix_attributes::NameRef::try_from(attr.as_bstr()).expect("valid attributes"),
                        state,
                    });
                } else {
                    unreachable!("invalid line format: {line:?}", line = line.as_bstr())
                }
            }
        }
    }
}
