// Re-export types from fallow-types
pub use fallow_types::suppress::{IssueKind, Suppression};

// Re-export parsing functions from fallow-extract
pub use fallow_extract::suppress::{parse_suppressions, parse_suppressions_from_source};

/// Check if a specific issue at a given line should be suppressed.
#[must_use]
pub fn is_suppressed(suppressions: &[Suppression], line: u32, kind: IssueKind) -> bool {
    suppressions.iter().any(|s| {
        // File-wide suppression
        if s.line == 0 {
            return s.kind.is_none() || s.kind == Some(kind);
        }
        // Line-specific suppression
        s.line == line && (s.kind.is_none() || s.kind == Some(kind))
    })
}

/// Check if the entire file is suppressed (for issue types that don't have line numbers).
#[must_use]
pub fn is_file_suppressed(suppressions: &[Suppression], kind: IssueKind) -> bool {
    suppressions
        .iter()
        .any(|s| s.line == 0 && (s.kind.is_none() || s.kind == Some(kind)))
}

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

    #[test]
    fn issue_kind_from_str_all_variants() {
        assert_eq!(IssueKind::parse("unused-file"), Some(IssueKind::UnusedFile));
        assert_eq!(
            IssueKind::parse("unused-export"),
            Some(IssueKind::UnusedExport)
        );
        assert_eq!(IssueKind::parse("unused-type"), Some(IssueKind::UnusedType));
        assert_eq!(
            IssueKind::parse("unused-dependency"),
            Some(IssueKind::UnusedDependency)
        );
        assert_eq!(
            IssueKind::parse("unused-dev-dependency"),
            Some(IssueKind::UnusedDevDependency)
        );
        assert_eq!(
            IssueKind::parse("unused-enum-member"),
            Some(IssueKind::UnusedEnumMember)
        );
        assert_eq!(
            IssueKind::parse("unused-class-member"),
            Some(IssueKind::UnusedClassMember)
        );
        assert_eq!(
            IssueKind::parse("unresolved-import"),
            Some(IssueKind::UnresolvedImport)
        );
        assert_eq!(
            IssueKind::parse("unlisted-dependency"),
            Some(IssueKind::UnlistedDependency)
        );
        assert_eq!(
            IssueKind::parse("duplicate-export"),
            Some(IssueKind::DuplicateExport)
        );
    }

    #[test]
    fn issue_kind_from_str_unknown() {
        assert_eq!(IssueKind::parse("foo"), None);
        assert_eq!(IssueKind::parse(""), None);
    }

    #[test]
    fn discriminant_roundtrip() {
        for kind in [
            IssueKind::UnusedFile,
            IssueKind::UnusedExport,
            IssueKind::UnusedType,
            IssueKind::UnusedDependency,
            IssueKind::UnusedDevDependency,
            IssueKind::UnusedEnumMember,
            IssueKind::UnusedClassMember,
            IssueKind::UnresolvedImport,
            IssueKind::UnlistedDependency,
            IssueKind::DuplicateExport,
            IssueKind::CodeDuplication,
            IssueKind::CircularDependency,
            IssueKind::TestOnlyDependency,
            IssueKind::BoundaryViolation,
        ] {
            assert_eq!(
                IssueKind::from_discriminant(kind.to_discriminant()),
                Some(kind)
            );
        }
        assert_eq!(IssueKind::from_discriminant(0), None);
        assert_eq!(IssueKind::from_discriminant(16), None);
    }

    #[test]
    fn parse_file_wide_suppression() {
        let source = "// fallow-ignore-file\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 0);
        assert!(suppressions[0].kind.is_none());
    }

    #[test]
    fn parse_file_wide_suppression_with_kind() {
        let source = "// fallow-ignore-file unused-export\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 0);
        assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
    }

    #[test]
    fn parse_next_line_suppression() {
        let source =
            "import { x } from './x';\n// fallow-ignore-next-line\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 3); // suppresses line 3 (the export)
        assert!(suppressions[0].kind.is_none());
    }

    #[test]
    fn parse_next_line_suppression_with_kind() {
        let source = "// fallow-ignore-next-line unused-export\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 2);
        assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
    }

    #[test]
    fn parse_unknown_kind_ignored() {
        let source = "// fallow-ignore-next-line typo-kind\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert!(suppressions.is_empty());
    }

    #[test]
    fn is_suppressed_file_wide() {
        let suppressions = vec![Suppression {
            line: 0,
            kind: None,
        }];
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
        assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedFile));
    }

    #[test]
    fn is_suppressed_file_wide_specific_kind() {
        let suppressions = vec![Suppression {
            line: 0,
            kind: Some(IssueKind::UnusedExport),
        }];
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
        assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedType));
    }

    #[test]
    fn is_suppressed_line_specific() {
        let suppressions = vec![Suppression {
            line: 5,
            kind: None,
        }];
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
        assert!(!is_suppressed(&suppressions, 6, IssueKind::UnusedExport));
    }

    #[test]
    fn is_suppressed_line_and_kind() {
        let suppressions = vec![Suppression {
            line: 5,
            kind: Some(IssueKind::UnusedExport),
        }];
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
        assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedType));
        assert!(!is_suppressed(&suppressions, 6, IssueKind::UnusedExport));
    }

    #[test]
    fn is_suppressed_empty() {
        assert!(!is_suppressed(&[], 5, IssueKind::UnusedExport));
    }

    #[test]
    fn is_file_suppressed_works() {
        let suppressions = vec![Suppression {
            line: 0,
            kind: None,
        }];
        assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));

        let suppressions = vec![Suppression {
            line: 0,
            kind: Some(IssueKind::UnusedFile),
        }];
        assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
        assert!(!is_file_suppressed(&suppressions, IssueKind::UnusedExport));

        // Line-specific suppression should not count as file-wide
        let suppressions = vec![Suppression {
            line: 5,
            kind: None,
        }];
        assert!(!is_file_suppressed(&suppressions, IssueKind::UnusedFile));
    }

    #[test]
    fn parse_oxc_comments() {
        use oxc_allocator::Allocator;
        use oxc_parser::Parser;
        use oxc_span::SourceType;

        let source = "// fallow-ignore-file\n// fallow-ignore-next-line unused-export\nexport const foo = 1;\nexport const bar = 2;\n";
        let allocator = Allocator::default();
        let parser_return = Parser::new(&allocator, source, SourceType::mjs()).parse();

        let suppressions = parse_suppressions(&parser_return.program.comments, source);
        assert_eq!(suppressions.len(), 2);

        // File-wide suppression
        assert_eq!(suppressions[0].line, 0);
        assert!(suppressions[0].kind.is_none());

        // Next-line suppression with kind
        assert_eq!(suppressions[1].line, 3); // suppresses line 3 (export const foo)
        assert_eq!(suppressions[1].kind, Some(IssueKind::UnusedExport));
    }

    #[test]
    fn parse_block_comment_suppression() {
        let source = "/* fallow-ignore-file */\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 0);
        assert!(suppressions[0].kind.is_none());
    }

    #[test]
    fn is_suppressed_multiple_suppressions_different_kinds() {
        let suppressions = vec![
            Suppression {
                line: 5,
                kind: Some(IssueKind::UnusedExport),
            },
            Suppression {
                line: 5,
                kind: Some(IssueKind::UnusedType),
            },
        ];
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedType));
        assert!(!is_suppressed(&suppressions, 5, IssueKind::UnusedFile));
    }

    #[test]
    fn is_suppressed_file_wide_blanket_and_specific_coexist() {
        let suppressions = vec![
            Suppression {
                line: 0,
                kind: Some(IssueKind::UnusedExport),
            },
            Suppression {
                line: 5,
                kind: None, // blanket suppress on line 5
            },
        ];
        // File-wide suppression only covers UnusedExport
        assert!(is_suppressed(&suppressions, 10, IssueKind::UnusedExport));
        assert!(!is_suppressed(&suppressions, 10, IssueKind::UnusedType));

        // Line 5 blanket suppression covers everything on line 5
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedType));
        assert!(is_suppressed(&suppressions, 5, IssueKind::UnusedExport));
    }

    #[test]
    fn is_file_suppressed_blanket_suppresses_all_kinds() {
        let suppressions = vec![Suppression {
            line: 0,
            kind: None, // blanket file-wide
        }];
        assert!(is_file_suppressed(&suppressions, IssueKind::UnusedFile));
        assert!(is_file_suppressed(&suppressions, IssueKind::UnusedExport));
        assert!(is_file_suppressed(&suppressions, IssueKind::UnusedType));
        assert!(is_file_suppressed(
            &suppressions,
            IssueKind::CircularDependency
        ));
        assert!(is_file_suppressed(
            &suppressions,
            IssueKind::CodeDuplication
        ));
    }

    #[test]
    fn is_file_suppressed_empty_list() {
        assert!(!is_file_suppressed(&[], IssueKind::UnusedFile));
    }

    #[test]
    fn parse_multiple_next_line_suppressions() {
        let source = "// fallow-ignore-next-line unused-export\nexport const foo = 1;\n// fallow-ignore-next-line unused-type\nexport type Bar = string;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 2);
        assert_eq!(suppressions[0].line, 2);
        assert_eq!(suppressions[0].kind, Some(IssueKind::UnusedExport));
        assert_eq!(suppressions[1].line, 4);
        assert_eq!(suppressions[1].kind, Some(IssueKind::UnusedType));
    }

    #[test]
    fn parse_code_duplication_suppression() {
        let source = "// fallow-ignore-file code-duplication\nexport const foo = 1;\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 0);
        assert_eq!(suppressions[0].kind, Some(IssueKind::CodeDuplication));
    }

    #[test]
    fn parse_circular_dependency_suppression() {
        let source = "// fallow-ignore-file circular-dependency\nimport { x } from './x';\n";
        let suppressions = parse_suppressions_from_source(source);
        assert_eq!(suppressions.len(), 1);
        assert_eq!(suppressions[0].line, 0);
        assert_eq!(suppressions[0].kind, Some(IssueKind::CircularDependency));
    }
}
