//! Integration test: `scope.include` enforces an allow-list.
//!
//! Semantics (gitignore convention):
//!   - `include` is empty Vec → all files pass through (backward-compat).
//!   - `include` is non-empty → only files matching at least one pattern
//!     are accepted. `exclude` rules still apply after include.
//!
//! Layout (built per-test in a TempDir):
//!   root/
//!   ├── a.md
//!   ├── b.py
//!   ├── c.png
//!   └── d.pdf

use std::fs;

use kebab_config::Config;
use kebab_core::{SourceConnector, SourceScope};
use kebab_source_fs::FsSourceConnector;

fn cfg_with_root(root: &str) -> Config {
    let mut c = Config::defaults();
    c.workspace.root = Some(root.to_string());
    c.workspace.exclude.clear();
    // Disable size / generated caps so small test files always pass.
    c.ingest.code.max_file_bytes = u64::MAX;
    c.ingest.code.max_file_lines = u32::MAX;
    c.ingest.code.skip_generated_header = false;
    c
}

fn setup_mixed_dir() -> tempfile::TempDir {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    fs::write(root.join("a.md"), b"md").unwrap();
    fs::write(root.join("b.py"), b"py").unwrap();
    fs::write(root.join("c.png"), b"\x89PNG").unwrap();
    fs::write(root.join("d.pdf"), b"%PDF").unwrap();
    dir
}

/// Empty include → all 4 files pass (backward-compat).
#[test]
fn include_empty_accepts_all_files() {
    let dir = setup_mixed_dir();
    let conn = FsSourceConnector::new(&cfg_with_root(dir.path().to_str().unwrap())).unwrap();
    let scope = SourceScope {
        include: vec![],
        ..SourceScope::default()
    };
    let assets = conn.scan(&scope).unwrap();
    let names: Vec<_> = assets.iter().map(|a| a.workspace_path.0.clone()).collect();
    assert!(
        names.contains(&"a.md".to_string()),
        "a.md missing; got: {names:?}"
    );
    assert!(
        names.contains(&"b.py".to_string()),
        "b.py missing; got: {names:?}"
    );
    assert!(
        names.contains(&"c.png".to_string()),
        "c.png missing; got: {names:?}"
    );
    assert!(
        names.contains(&"d.pdf".to_string()),
        "d.pdf missing; got: {names:?}"
    );
    assert_eq!(names.len(), 4, "expected exactly 4 files; got: {names:?}");
}

/// Non-empty include → only md + py come back; png + pdf are excluded.
#[test]
fn include_nonempty_is_allowlist() {
    let dir = setup_mixed_dir();
    let conn = FsSourceConnector::new(&cfg_with_root(dir.path().to_str().unwrap())).unwrap();
    let scope = SourceScope {
        include: vec!["**/*.md".to_string(), "**/*.py".to_string()],
        ..SourceScope::default()
    };
    let assets = conn.scan(&scope).unwrap();
    let names: Vec<_> = assets.iter().map(|a| a.workspace_path.0.clone()).collect();
    assert!(
        names.contains(&"a.md".to_string()),
        "a.md should be accepted; got: {names:?}"
    );
    assert!(
        names.contains(&"b.py".to_string()),
        "b.py should be accepted; got: {names:?}"
    );
    assert!(
        !names.contains(&"c.png".to_string()),
        "c.png must be rejected by include allowlist; got: {names:?}"
    );
    assert!(
        !names.contains(&"d.pdf".to_string()),
        "d.pdf must be rejected by include allowlist; got: {names:?}"
    );
    assert_eq!(names.len(), 2, "expected exactly 2 files; got: {names:?}");
}

/// include + exclude are ANDed: a file matching include but also matching
/// exclude must be rejected.
#[test]
fn include_and_exclude_are_anded() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path();
    fs::write(root.join("keep.md"), b"keep").unwrap();
    fs::write(root.join("drop.md"), b"drop").unwrap();
    fs::write(root.join("other.py"), b"py").unwrap();

    let conn = FsSourceConnector::new(&cfg_with_root(root.to_str().unwrap())).unwrap();
    let scope = SourceScope {
        include: vec!["**/*.md".to_string()],
        exclude: vec!["drop.md".to_string()],
        ..SourceScope::default()
    };
    let assets = conn.scan(&scope).unwrap();
    let names: Vec<_> = assets.iter().map(|a| a.workspace_path.0.clone()).collect();
    assert!(
        names.contains(&"keep.md".to_string()),
        "keep.md should be accepted; got: {names:?}"
    );
    assert!(
        !names.contains(&"drop.md".to_string()),
        "drop.md should be excluded (matched exclude); got: {names:?}"
    );
    assert!(
        !names.contains(&"other.py".to_string()),
        "other.py should be excluded (not in include); got: {names:?}"
    );
}
