//! Within-kind overlap detection.
//!
//! Two routes "conflict" if they're the same kind AND there exists at
//! least one request that both would match. Cross-kind overlap is OK —
//! the runtime matcher resolves it via the precedence rule
//! (`exact > param > prefix`, with leading-literal specificity).
//!
//! The user's design intent (see chat history): "There should only be
//! one eligible route in each domain for a path within the same kind."
//! This file is the predicate that enforces it at write time.

use super::pattern::{HostPattern, PathPattern, PathSegment};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConflictReason {
    /// Two exact paths with identical literal value.
    IdenticalExact,
    /// Two prefix paths with identical literal value (different
    /// lengths coexist; the longer wins at runtime).
    IdenticalPrefix,
    /// Two param paths with the same shape (same segment count and
    /// matching literals at every literal-vs-literal position).
    OverlappingParam,
}

/// True if these two patterns conflict (within-kind only). Callers
/// should already have decided the host/method dimensions overlap.
#[must_use]
pub fn conflicts(a: &PathPattern, b: &PathPattern) -> Option<ConflictReason> {
    match (a, b) {
        (PathPattern::Exact(x), PathPattern::Exact(y)) if x == y => {
            Some(ConflictReason::IdenticalExact)
        }
        (PathPattern::Prefix(x), PathPattern::Prefix(y)) if x == y => {
            Some(ConflictReason::IdenticalPrefix)
        }
        (PathPattern::Param(xs), PathPattern::Param(ys)) if param_overlap(xs, ys) => {
            Some(ConflictReason::OverlappingParam)
        }
        _ => None,
    }
}

/// Two host patterns "share a domain bucket" — i.e., a request to one
/// host satisfies both — exactly when their parsed forms agree on kind
/// and value. Conflict-checks across different domain buckets do not
/// apply (the runtime host precedence picks one bucket per request).
#[must_use]
pub fn hosts_overlap(a: &HostPattern, b: &HostPattern) -> bool {
    match (a, b) {
        (HostPattern::Any, HostPattern::Any) => true,
        (HostPattern::Strict(x), HostPattern::Strict(y)) => x == y,
        (HostPattern::Wildcard { suffix: x, .. }, HostPattern::Wildcard { suffix: y, .. }) => {
            x == y
        }
        _ => false,
    }
}

/// True if the two HTTP methods would dispatch to the same route.
/// `None` = "any method"; two Anys overlap, an Any overlaps any
/// specific method, and two specifics overlap iff equal.
#[must_use]
pub fn methods_overlap(a: Option<&str>, b: Option<&str>) -> bool {
    match (a, b) {
        (None, _) | (_, None) => true,
        (Some(x), Some(y)) => x.eq_ignore_ascii_case(y),
    }
}

fn param_overlap(a: &[PathSegment], b: &[PathSegment]) -> bool {
    if a.len() != b.len() {
        return false;
    }
    a.iter().zip(b.iter()).all(|(x, y)| match (x, y) {
        (PathSegment::Literal(p), PathSegment::Literal(q)) => p == q,
        // Any pair involving at least one Param accepts overlap.
        _ => true,
    })
}

// ----------------------------------------------------------------------------
// Tests
// ----------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::super::pattern::{parse_path, PathPattern};
    use super::*;
    use picloud_shared::PathKind;

    fn p(kind: PathKind, raw: &str) -> PathPattern {
        parse_path(kind, raw).unwrap()
    }

    #[test]
    fn identical_exacts_conflict() {
        assert_eq!(
            conflicts(&p(PathKind::Exact, "/x"), &p(PathKind::Exact, "/x")),
            Some(ConflictReason::IdenticalExact)
        );
    }

    #[test]
    fn different_exacts_dont_conflict() {
        assert_eq!(
            conflicts(&p(PathKind::Exact, "/x"), &p(PathKind::Exact, "/y")),
            None
        );
    }

    #[test]
    fn identical_prefixes_conflict() {
        assert_eq!(
            conflicts(
                &p(PathKind::Prefix, "/greet/*"),
                &p(PathKind::Prefix, "/greet/*")
            ),
            Some(ConflictReason::IdenticalPrefix)
        );
    }

    #[test]
    fn different_length_prefixes_dont_conflict() {
        assert_eq!(
            conflicts(
                &p(PathKind::Prefix, "/greet/*"),
                &p(PathKind::Prefix, "/greet/sub/*")
            ),
            None
        );
    }

    #[test]
    fn cross_kind_never_conflicts() {
        assert_eq!(
            conflicts(&p(PathKind::Exact, "/x"), &p(PathKind::Prefix, "/x/*")),
            None
        );
        assert_eq!(
            conflicts(
                &p(PathKind::Exact, "/greet/alice"),
                &p(PathKind::Param, "/greet/:name")
            ),
            None
        );
        assert_eq!(
            conflicts(
                &p(PathKind::Param, "/greet/:name"),
                &p(PathKind::Prefix, "/greet/*")
            ),
            None
        );
    }

    #[test]
    fn same_shape_params_conflict_regardless_of_name() {
        assert_eq!(
            conflicts(
                &p(PathKind::Param, "/users/:id"),
                &p(PathKind::Param, "/users/:userId")
            ),
            Some(ConflictReason::OverlappingParam)
        );
    }

    #[test]
    fn param_shapes_with_overlapping_literals_conflict() {
        // /users/admin/posts is reachable by both.
        assert_eq!(
            conflicts(
                &p(PathKind::Param, "/users/:id/posts"),
                &p(PathKind::Param, "/users/admin/:action")
            ),
            Some(ConflictReason::OverlappingParam)
        );
    }

    #[test]
    fn param_with_distinct_trailing_literal_no_conflict() {
        assert_eq!(
            conflicts(
                &p(PathKind::Param, "/users/:id/posts"),
                &p(PathKind::Param, "/users/:id/comments")
            ),
            None
        );
    }

    #[test]
    fn params_of_different_lengths_no_conflict() {
        assert_eq!(
            conflicts(
                &p(PathKind::Param, "/users/:id"),
                &p(PathKind::Param, "/users/:id/posts")
            ),
            None
        );
    }

    #[test]
    fn methods_any_overlaps_anything() {
        assert!(methods_overlap(None, None));
        assert!(methods_overlap(None, Some("GET")));
        assert!(methods_overlap(Some("POST"), None));
    }

    #[test]
    fn methods_case_insensitive() {
        assert!(methods_overlap(Some("GET"), Some("get")));
    }

    #[test]
    fn methods_different_specifics_no_overlap() {
        assert!(!methods_overlap(Some("GET"), Some("POST")));
    }

    #[test]
    fn hosts_strict_strict() {
        let a = HostPattern::Strict("a.com".into());
        let b = HostPattern::Strict("a.com".into());
        let c = HostPattern::Strict("b.com".into());
        assert!(hosts_overlap(&a, &b));
        assert!(!hosts_overlap(&a, &c));
    }

    #[test]
    fn hosts_strict_vs_wildcard_different_buckets() {
        let s = HostPattern::Strict("sub.example.com".into());
        let w = HostPattern::Wildcard {
            suffix: "example.com".into(),
            capture: None,
        };
        // They CAN match the same request at runtime; specificity
        // picks one. They're not in the same conflict bucket.
        assert!(!hosts_overlap(&s, &w));
    }
}
