use serde::{Deserialize, Serialize};
use unicode_normalization::UnicodeNormalization;
use sha2::{Digest, Sha256};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum NormalizationResult {
    Normalized(String),
    Invalid(String),
}

pub trait Normalizer: Send + Sync {
    fn normalize(&self, value: &str) -> NormalizationResult;
}

pub struct EmailNormalizer;
impl Normalizer for EmailNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim().to_lowercase();
        let parts: Vec<&str> = trimmed.splitn(2, '@').collect();
        if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
            return NormalizationResult::Invalid("Invalid email format".to_string());
        }

        let (local, domain) = (parts[0], parts[1]);

        let normalized_local = if domain == "gmail.com" || domain == "googlemail.com" {
            let without_plus = local.split('+').next().unwrap_or(local);
            without_plus.replace('.', "")
        } else {
            local.to_string()
        };

        NormalizationResult::Normalized(format!("{}@{}", normalized_local, domain))
    }
}

/// ITU-T E.164 country calling codes, sorted for binary search.
/// Covers all assigned codes as of 2025. E.164 codes are prefix-free:
/// no valid code is a prefix of another, so shortest-match-first is unambiguous.
const E164_COUNTRY_CODES: &[u16] = &[
    // 1-digit
    1, 7,
    // 2-digit
    20, 27, 30, 31, 32, 33, 34, 36, 39,
    40, 41, 43, 44, 45, 46, 47, 48, 49,
    51, 52, 53, 54, 55, 56, 57, 58,
    60, 61, 62, 63, 64, 65, 66,
    81, 82, 84, 86,
    90, 91, 92, 93, 94, 95, 98,
    // 3-digit: Africa
    211, 212, 213, 216, 218,
    220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
    230, 231, 232, 233, 234, 235, 236, 237, 238, 239,
    240, 241, 242, 243, 244, 245, 246, 247, 248, 249,
    250, 251, 252, 253, 254, 255, 256, 257, 258,
    260, 261, 262, 263, 264, 265, 266, 267, 268, 269,
    290, 291, 297, 298, 299,
    // 3-digit: Europe
    350, 351, 352, 353, 354, 355, 356, 357, 358, 359,
    370, 371, 372, 373, 374, 375, 376, 377, 378,
    380, 381, 382, 383, 385, 386, 387, 389,
    420, 421, 423,
    // 3-digit: Americas
    500, 501, 502, 503, 504, 505, 506, 507, 508, 509,
    590, 591, 592, 593, 594, 595, 596, 597, 598, 599,
    // 3-digit: Asia-Pacific
    670, 672, 673, 674, 675, 676, 677, 678, 679,
    680, 681, 682, 683, 684, 685, 686, 687, 688, 689,
    690, 691, 692,
    // 3-digit: East Asia, satellite, global services
    800, 808,
    850, 852, 853, 855, 856, 870, 878,
    880, 881, 882, 883, 886, 888,
    // 3-digit: Middle East, Central/South Asia
    960, 961, 962, 963, 964, 965, 966, 967, 968,
    970, 971, 972, 973, 974, 975, 976, 977, 979,
    992, 993, 994, 995, 996, 998,
];

fn is_valid_country_code(code: u16) -> bool {
    E164_COUNTRY_CODES.binary_search(&code).is_ok()
}

/// Extract a valid E.164 country code from the start of a digit string.
/// Returns (code, digit_length). Tries 1-digit, then 2-digit, then 3-digit
/// - correct because E.164 codes are prefix-free.
fn extract_country_code(digits: &str) -> Option<(u16, usize)> {
    let max_len = 3.min(digits.len());
    for len in 1..=max_len {
        if let Ok(code) = digits[..len].parse::<u16>() {
            if is_valid_country_code(code) {
                return Some((code, len));
            }
        }
    }
    None
}

pub struct PhoneNormalizer;
impl Normalizer for PhoneNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim();
        let has_plus = trimmed.starts_with('+');
        let digits: String = trimmed.chars().filter(|c| c.is_ascii_digit()).collect();

        if digits.is_empty() {
            return NormalizationResult::Invalid("No digits found".to_string());
        }

        // Step 1: Determine the international digit string (country code + subscriber).
        // Strip international dialing prefixes (IDD) and trunk prefixes when present.
        let international = if has_plus {
            // +{cc}{subscriber} - already international format
            digits.as_str()
        } else if digits.starts_with("011") && digits.len() >= 11 {
            // US/Canada IDD prefix: 011 + cc + subscriber
            &digits[3..]
        } else if digits.starts_with("00") && digits.len() >= 10 {
            // ITU standard IDD prefix (most countries): 00 + cc + subscriber
            &digits[2..]
        } else if digits.starts_with('0') && digits.len() >= 10 {
            // Leading 0 is a trunk prefix used in many countries (UK, France,
            // Germany, Japan, etc.). Strip it and normalize the remainder.
            // Without country context we can't know the true CC, but stripping
            // the trunk prefix produces a consistent key for same-source matching.
            &digits[1..]
        } else if digits.len() == 10 {
            // 10 digits, no leading zero -> assume NANP (+1)
            return NormalizationResult::Normalized(format!("+1{}", digits));
        } else if digits.len() >= 11 && digits.len() <= 15 {
            // 11-15 digits without prefix -> assume cc + subscriber
            digits.as_str()
        } else if digits.len() < 10 {
            return NormalizationResult::Invalid("Too few digits for E.164".to_string());
        } else {
            return NormalizationResult::Invalid("Too many digits for E.164".to_string());
        };

        // Step 2: Validate E.164 constraints.
        // Country codes never start with 0. Total max 15 digits.
        if international.starts_with('0') {
            return NormalizationResult::Invalid(
                "Invalid E.164: country codes never start with 0".to_string(),
            );
        }
        if international.len() > 15 {
            return NormalizationResult::Invalid("Too many digits for E.164".to_string());
        }
        if international.len() < 7 {
            return NormalizationResult::Invalid("Too few digits for E.164".to_string());
        }

        // Step 3: Validate that the leading digits are a real country code.
        if extract_country_code(international).is_none() {
            return NormalizationResult::Invalid("Unrecognized country code".to_string());
        }

        NormalizationResult::Normalized(format!("+{}", international))
    }
}

pub struct NameNormalizer;
impl Normalizer for NameNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let nfc: String = value.nfc().collect();
        let lowered = nfc.to_lowercase();
        let normalized = lowered
            .split_whitespace()
            .collect::<Vec<_>>()
            .join(" ");
        NormalizationResult::Normalized(normalized)
    }
}

pub struct DomainNormalizer;
impl Normalizer for DomainNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let mut normalized = value.trim().to_lowercase();
        normalized = normalized
            .strip_prefix("https://")
            .or_else(|| normalized.strip_prefix("http://"))
            .unwrap_or(&normalized)
            .to_string();
        normalized = normalized.strip_prefix("www.").unwrap_or(&normalized).to_string();
        if let Some(pos) = normalized.find('/') {
            normalized = normalized[..pos].to_string();
        }
        NormalizationResult::Normalized(normalized)
    }
}

/// Nickname normalizer: applies NameNormalizer, then maps common nicknames
/// to their canonical form (e.g., "bob" → "robert", "liz" → "elizabeth").
/// This closes the gap with Splink's nickname resolution preprocessing.
pub struct NicknameNormalizer;
impl NicknameNormalizer {
    fn canonical(name: &str) -> &str {
        match name {
            "bob" | "rob" | "robbie" | "robby" | "bobby" => "robert",
            "bill" | "will" | "willy" | "willie" | "billy" | "liam" => "william",
            "dick" | "rick" | "rich" | "richie" | "ricky" => "richard",
            "jim" | "jimmy" | "jamie" => "james",
            "mike" | "mikey" | "mick" => "michael",
            "jen" | "jenny" => "jennifer",
            "liz" | "beth" | "betty" | "betsy" | "eliza" | "lizzy" => "elizabeth",
            "pat" | "patty" | "tricia" | "trish" => "patricia",
            "chris" => "christopher",
            "kate" | "kathy" | "katie" | "kat" | "kath" => "katherine",
            "ben" | "benny" => "benjamin",
            "nick" | "nicky" => "nicholas",
            "tom" | "tommy" => "thomas",
            "dan" | "danny" => "daniel",
            "dave" | "davy" => "david",
            "steve" | "stephen" => "steven",
            "joe" | "joey" => "joseph",
            "tony" => "anthony",
            "ed" | "eddie" | "ted" | "teddy" | "ned" => "edward",
            "sam" | "sammy" => "samuel",
            "matt" | "matty" => "matthew",
            "andy" | "drew" => "andrew",
            "alex" | "xander" => "alexander",
            "charlie" | "chuck" | "chaz" => "charles",
            "harry" | "hank" | "hal" => "henry",
            "jack" | "johnny" => "john",
            "larry" => "lawrence",
            "jerry" => "gerald",
            "terry" => "terrence",
            "ray" => "raymond",
            "al" => "alan",
            "meg" | "maggie" | "peggy" | "marge" => "margaret",
            "sue" | "susie" | "suzy" => "susan",
            "deb" | "debbie" | "debby" => "deborah",
            "barb" | "babs" => "barbara",
            "cathy" | "cath" => "catherine",
            "vicky" | "vicki" => "victoria",
            "mandy" => "amanda",
            "becky" => "rebecca",
            "cindy" => "cynthia",
            "sandy" => "sandra",
            "wendy" => "gwendolyn",
            "nate" => "nathan",
            "walt" => "walter",
            "fred" | "freddy" => "frederick",
            "frank" | "frankie" => "francis",
            "phil" => "philip",
            "ron" | "ronnie" => "ronald",
            "don" | "donnie" => "donald",
            "doug" => "douglas",
            "greg" => "gregory",
            "jeff" => "jeffrey",
            "ken" | "kenny" => "kenneth",
            "tim" | "timmy" => "timothy",
            "abe" => "abraham",
            "gus" => "augustus",
            "pete" => "peter",
            "gene" => "eugene",
            "ernie" => "ernest",
            "archie" => "archibald",
            "lenny" | "len" => "leonard",
            _ => name,
        }
    }
}
impl Normalizer for NicknameNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        // First apply standard name normalization (NFC, lowercase, whitespace)
        let name_result = NameNormalizer.normalize(value);
        match name_result {
            NormalizationResult::Normalized(normalized) => {
                let canonical = Self::canonical(&normalized);
                NormalizationResult::Normalized(canonical.to_string())
            }
            other => other,
        }
    }
}

pub struct GenericNormalizer;
impl Normalizer for GenericNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        NormalizationResult::Normalized(value.trim().to_lowercase())
    }
}

// ---------------------------------------------------------------------------
// Product identity normalizers
// ---------------------------------------------------------------------------

/// Barcode/GTIN normalizer: strips formatting, validates check digits,
/// and normalizes UPC-A (12), EAN-13, GTIN-8, GTIN-14 to canonical EAN-13.
///
/// GS1 check-digit algorithm: alternating weights 1 and 3 from right,
/// check digit = (10 - (sum mod 10)) mod 10.
pub struct BarcodeNormalizer;

impl BarcodeNormalizer {
    /// Compute the GS1 check digit for a digit string of length N-1.
    /// The check digit makes the weighted sum of all N digits divisible by 10.
    fn gs1_check_digit(digits: &[u8]) -> u8 {
        let len = digits.len();
        let mut sum: u32 = 0;
        // Weights alternate 1, 3 from the rightmost position (the check digit position).
        // For the digits preceding the check digit, the rightmost has weight 3, next 1, etc.
        for (i, &d) in digits.iter().enumerate() {
            let weight = if (len - 1 - i) % 2 == 0 { 3 } else { 1 };
            sum += (d as u32) * weight;
        }
        ((10 - (sum % 10)) % 10) as u8
    }

    /// Validate that the last digit is the correct GS1 check digit.
    fn validate_check_digit(digits: &[u8]) -> bool {
        if digits.is_empty() {
            return false;
        }
        let payload = &digits[..digits.len() - 1];
        let expected = Self::gs1_check_digit(payload);
        digits[digits.len() - 1] == expected
    }

    /// Parse a string into a vector of digit values.
    fn parse_digits(s: &str) -> Option<Vec<u8>> {
        let digits: Vec<u8> = s.chars()
            .filter(|c| c.is_ascii_digit())
            .map(|c| c as u8 - b'0')
            .collect();
        if digits.is_empty() { None } else { Some(digits) }
    }
}

impl Normalizer for BarcodeNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim();

        // ISBN-10 check digit can be 'X' (=10). Detect and handle before
        // parse_digits which strips non-digit characters.
        // Strip hyphens/spaces first so "0-306-40615-X" is recognized.
        let stripped: String = trimmed.chars()
            .filter(|c| c.is_ascii_alphanumeric())
            .collect();
        let upper = stripped.to_uppercase();
        let is_isbn10_with_x = upper.len() == 10
            && upper.ends_with('X')
            && upper[..9].chars().all(|c| c.is_ascii_digit());
        if is_isbn10_with_x {
            // Validate ISBN-10 check digit: weights 10,9,...,2,1; sum mod 11 == 0; X=10
            let isbn_digits: Vec<u8> = upper[..9].chars().map(|c| c as u8 - b'0').collect();
            let mut sum: u32 = 0;
            for (i, &d) in isbn_digits.iter().enumerate() {
                sum += (d as u32) * (10 - i as u32);
            }
            sum += 10; // X check digit
            if sum % 11 != 0 {
                return NormalizationResult::Invalid("Invalid ISBN-10 check digit".to_string());
            }
            // Convert to ISBN-13: "978" + first 9 digits + new GS1 check digit
            let mut isbn13: Vec<u8> = vec![9, 7, 8];
            isbn13.extend_from_slice(&isbn_digits);
            let check = Self::gs1_check_digit(&isbn13);
            isbn13.push(check);
            let s: String = isbn13.iter().map(|d| (d + b'0') as char).collect();
            return NormalizationResult::Normalized(s);
        }

        let digits = match Self::parse_digits(trimmed) {
            Some(d) => d,
            None => return NormalizationResult::Invalid("No digits found".to_string()),
        };

        match digits.len() {
            // GTIN-8 (EAN-8): validate, then convert to EAN-13 by prepending zeros
            // and recalculating the check digit so cross-format matching works.
            8 => {
                if !Self::validate_check_digit(&digits) {
                    return NormalizationResult::Invalid("Invalid GTIN-8 check digit".to_string());
                }
                // Take first 7 digits (payload without check), prepend 5 zeros -> 12-digit EAN-13 payload
                let mut ean13_payload: Vec<u8> = vec![0; 5];
                ean13_payload.extend_from_slice(&digits[..7]);
                let check = Self::gs1_check_digit(&ean13_payload);
                ean13_payload.push(check);
                let s: String = ean13_payload.iter().map(|d| (d + b'0') as char).collect();
                NormalizationResult::Normalized(s)
            }
            // UPC-A (12 digits): validate and convert to EAN-13 by prepending 0
            12 => {
                if !Self::validate_check_digit(&digits) {
                    return NormalizationResult::Invalid("Invalid UPC-A check digit".to_string());
                }
                let s: String = digits.iter().map(|d| (d + b'0') as char).collect();
                NormalizationResult::Normalized(format!("0{}", s))
            }
            // EAN-13: validate check digit, output as-is
            13 => {
                if !Self::validate_check_digit(&digits) {
                    return NormalizationResult::Invalid("Invalid EAN-13 check digit".to_string());
                }
                let s: String = digits.iter().map(|d| (d + b'0') as char).collect();
                NormalizationResult::Normalized(s)
            }
            // GTIN-14: validate, then extract the inner item reference (digits 2-13)
            // and recalculate the EAN-13 check digit for cross-format matching.
            14 => {
                if !Self::validate_check_digit(&digits) {
                    return NormalizationResult::Invalid("Invalid GTIN-14 check digit".to_string());
                }
                // Strip the packaging indicator (first digit), take next 12 as EAN-13 payload
                let ean13_payload: Vec<u8> = digits[1..13].to_vec();
                let check = Self::gs1_check_digit(&ean13_payload);
                let mut ean13 = ean13_payload;
                ean13.push(check);
                let s: String = ean13.iter().map(|d| (d + b'0') as char).collect();
                NormalizationResult::Normalized(s)
            }
            // ISBN-10 (10 all-digit): validate check digit, then convert to ISBN-13.
            // X check digits are handled above before parse_digits.
            10 => {
                // ISBN-10 check: weights 10,9,...,2,1; sum mod 11 == 0
                let mut sum: u32 = 0;
                for (i, &d) in digits.iter().enumerate() {
                    sum += (d as u32) * (10 - i as u32);
                }
                if sum % 11 != 0 {
                    return NormalizationResult::Invalid("Invalid ISBN-10 check digit".to_string());
                }
                // ISBN-13 = "978" + first 9 digits of ISBN-10 + new GS1 check digit
                let isbn9: Vec<u8> = digits[..9].to_vec();
                let mut isbn13: Vec<u8> = vec![9, 7, 8];
                isbn13.extend_from_slice(&isbn9);
                let check = Self::gs1_check_digit(&isbn13);
                isbn13.push(check);
                let s: String = isbn13.iter().map(|d| (d + b'0') as char).collect();
                NormalizationResult::Normalized(s)
            }
            _ => NormalizationResult::Invalid(
                format!("Unsupported barcode length: {} digits", digits.len()),
            ),
        }
    }
}

/// MPN (Manufacturer Part Number) normalizer: uppercase, strip separators,
/// collapse whitespace for consistent matching.
pub struct MpnNormalizer;
impl Normalizer for MpnNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return NormalizationResult::Invalid("Empty MPN".to_string());
        }
        let uppercased = trimmed.to_uppercase();
        // Strip hyphens, dots, spaces, underscores
        let stripped: String = uppercased.chars()
            .filter(|c| !matches!(c, '-' | '.' | ' ' | '_'))
            .collect();
        // Trim leading zeros (but keep at least one char)
        let result = stripped.trim_start_matches('0');
        let result = if result.is_empty() { "0" } else { result };
        NormalizationResult::Normalized(result.to_string())
    }
}

/// Brand normalizer: lowercase, strip legal suffixes (Inc, LLC, Ltd, etc.),
/// normalize common patterns for consistent brand matching.
pub struct BrandNormalizer;

impl BrandNormalizer {
    /// Legal suffixes to strip from brand names (case-insensitive).
    const LEGAL_SUFFIXES: &[&str] = &[
        "inc", "incorporated", "llc", "ltd", "limited", "corp", "corporation",
        "co", "company", "gmbh", "sa", "ag", "plc", "nv", "bv", "pty",
        "srl", "spa", "ab", "as", "oy", "se",
    ];
}

impl Normalizer for BrandNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        let trimmed = value.trim();
        if trimmed.is_empty() {
            return NormalizationResult::Invalid("Empty brand".to_string());
        }

        let mut normalized = trimmed.to_lowercase();

        // Strip "the " prefix
        if let Some(rest) = normalized.strip_prefix("the ") {
            normalized = rest.to_string();
        }

        // Normalize "&" to "and"
        normalized = normalized.replace(" & ", " and ");

        // Strip trailing legal suffixes (with optional preceding punctuation)
        for suffix in Self::LEGAL_SUFFIXES {
            // Try with comma: "Apple, Inc" or "Apple, Inc."
            let with_comma = format!(", {}.", suffix);
            if normalized.ends_with(&with_comma) {
                let end = normalized.len() - with_comma.len();
                normalized = normalized[..end].to_string();
                break;
            }
            let with_comma_no_dot = format!(", {}", suffix);
            if normalized.ends_with(&with_comma_no_dot) {
                let end = normalized.len() - with_comma_no_dot.len();
                normalized = normalized[..end].to_string();
                break;
            }
            // Try without comma: "Apple Inc" or "Apple Inc."
            let with_space_dot = format!(" {}.", suffix);
            if normalized.ends_with(&with_space_dot) {
                let end = normalized.len() - with_space_dot.len();
                normalized = normalized[..end].to_string();
                break;
            }
            let with_space = format!(" {}", suffix);
            if normalized.ends_with(&with_space) {
                let end = normalized.len() - with_space.len();
                normalized = normalized[..end].to_string();
                break;
            }
        }

        // Trim any trailing whitespace or punctuation left over
        let normalized = normalized.trim_end_matches(|c: char| c == '.' || c == ',' || c.is_whitespace());

        NormalizationResult::Normalized(normalized.to_string())
    }
}

pub struct HashedNormalizer {
    pub inner: Box<dyn Normalizer>,
    pub salt: String,
}

impl Normalizer for HashedNormalizer {
    fn normalize(&self, value: &str) -> NormalizationResult {
        match self.inner.normalize(value) {
            NormalizationResult::Normalized(n) => {
                let mut hasher = Sha256::new();
                hasher.update(self.salt.as_bytes());
                hasher.update(n.as_bytes());
                let result = hasher.finalize();
                NormalizationResult::Normalized(hex::encode(result))
            }
            res => res,
        }
    }
}

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

    fn assert_phone(input: &str, expected: &str) {
        match PhoneNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                assert_eq!(result, expected, "Input: {:?}", input);
            }
            NormalizationResult::Invalid(reason) => {
                panic!("Expected {:?} for {:?}, got Invalid({:?})", expected, input, reason);
            }
        }
    }

    fn assert_phone_invalid(input: &str) {
        match PhoneNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                panic!("Expected Invalid for {:?}, got Normalized({:?})", input, result);
            }
            NormalizationResult::Invalid(_) => {}
        }
    }

    // -- US/NANP numbers (country code 1) --

    #[test]
    fn phone_us_10_digits() {
        assert_phone("5551234567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_parens() {
        assert_phone("(555) 123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_dashes() {
        assert_phone("555-123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_formatted_dots() {
        assert_phone("555.123.4567", "+15551234567");
    }

    #[test]
    fn phone_us_with_country_code() {
        assert_phone("15551234567", "+15551234567");
    }

    #[test]
    fn phone_us_e164() {
        assert_phone("+15551234567", "+15551234567");
    }

    #[test]
    fn phone_us_e164_formatted() {
        assert_phone("+1-555-123-4567", "+15551234567");
    }

    #[test]
    fn phone_us_with_spaces() {
        assert_phone("  +1 555 123 4567  ", "+15551234567");
    }

    // -- UK numbers (country code 44) --

    #[test]
    fn phone_uk_e164() {
        assert_phone("+442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_e164_formatted() {
        assert_phone("+44 20 7946 0958", "+442079460958");
    }

    #[test]
    fn phone_uk_without_plus() {
        assert_phone("442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_idd_00() {
        assert_phone("00442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_idd_011() {
        assert_phone("011442079460958", "+442079460958");
    }

    #[test]
    fn phone_uk_mobile_e164() {
        assert_phone("+447700900000", "+447700900000");
    }

    // -- German numbers (country code 49) --

    #[test]
    fn phone_germany_e164() {
        assert_phone("+49891234567", "+49891234567");
    }

    #[test]
    fn phone_germany_idd_00() {
        assert_phone("0049891234567", "+49891234567");
    }

    // -- Japanese numbers (country code 81) --

    #[test]
    fn phone_japan_e164() {
        assert_phone("+81312345678", "+81312345678");
    }

    #[test]
    fn phone_japan_without_plus() {
        assert_phone("81312345678", "+81312345678");
    }

    // -- Indian numbers (country code 91) --

    #[test]
    fn phone_india_e164() {
        assert_phone("+919876543210", "+919876543210");
    }

    #[test]
    fn phone_india_without_plus() {
        assert_phone("919876543210", "+919876543210");
    }

    // -- Australian numbers (country code 61) --

    #[test]
    fn phone_australia_e164() {
        assert_phone("+61412345678", "+61412345678");
    }

    // -- Chinese numbers (country code 86) --

    #[test]
    fn phone_china_e164() {
        assert_phone("+8613800138000", "+8613800138000");
    }

    // -- Brazilian numbers (country code 55) --

    #[test]
    fn phone_brazil_e164() {
        assert_phone("+5511987654321", "+5511987654321");
    }

    // -- South Korean numbers (country code 82) --

    #[test]
    fn phone_korea_e164() {
        assert_phone("+821012345678", "+821012345678");
    }

    // -- IDD prefix handling --

    #[test]
    fn phone_idd_00_to_us() {
        assert_phone("0015551234567", "+15551234567");
    }

    #[test]
    fn phone_idd_011_to_uk() {
        assert_phone("011442079460958", "+442079460958");
    }

    #[test]
    fn phone_idd_00_to_india() {
        assert_phone("00919876543210", "+919876543210");
    }

    // -- Cross-format matching (same number, different formats -> same output) --

    #[test]
    fn phone_cross_format_us() {
        let formats = [
            "5551234567",
            "(555) 123-4567",
            "555-123-4567",
            "+15551234567",
            "+1-555-123-4567",
            "15551234567",
            "0015551234567",
        ];
        for fmt in &formats {
            assert_phone(fmt, "+15551234567");
        }
    }

    #[test]
    fn phone_cross_format_uk() {
        let formats = [
            "+442079460958",
            "+44 20 7946 0958",
            "442079460958",
            "00442079460958",
            "011442079460958",
        ];
        for fmt in &formats {
            assert_phone(fmt, "+442079460958");
        }
    }

    // -- Idempotence: normalizing an already-normalized number --

    #[test]
    fn phone_idempotent_us() {
        assert_phone("+15551234567", "+15551234567");
    }

    #[test]
    fn phone_idempotent_uk() {
        assert_phone("+442079460958", "+442079460958");
    }

    // -- Invalid inputs --

    #[test]
    fn phone_invalid_empty() {
        assert_phone_invalid("");
    }

    #[test]
    fn phone_invalid_no_digits() {
        assert_phone_invalid("abc-def-ghij");
    }

    #[test]
    fn phone_invalid_too_short() {
        assert_phone_invalid("12345");
    }

    #[test]
    fn phone_invalid_too_long() {
        assert_phone_invalid("1234567890123456789");
    }

    // -- Trunk prefix stripping (leading 0) --

    #[test]
    fn phone_uk_local_trunk_prefix() {
        // UK local: 020 7946 0958 -> strip 0 -> 2079460958 -> CC=20, normalized
        assert_phone("02079460958", "+2079460958");
    }

    #[test]
    fn phone_uk_local_formatted() {
        // Same UK local with formatting -> same result
        assert_phone("020 7946 0958", "+2079460958");
    }

    #[test]
    fn phone_french_local_trunk_prefix() {
        // French local: 06 12 34 56 78 -> strip 0 -> 612345678 -> CC=61, normalized
        assert_phone("0612345678", "+612345678");
    }

    #[test]
    fn phone_uk_mobile_local() {
        // UK mobile local: 07700 900000 -> strip 0 -> 7700900000 -> CC=7, normalized
        assert_phone("07700900000", "+7700900000");
    }

    #[test]
    fn phone_german_local_trunk_prefix() {
        // German local: 089 1234567 -> strip 0 -> 891234567 -> CC=86? No, 8 invalid, 89 invalid.
        // Actually 891234567 = 9 digits. CC try: 8 (invalid), 89 (invalid), 891 (invalid) -> invalid
        // Wait - let me check: is there a CC starting with 89? No. So this would be invalid.
        // But German local 089 has 10 digits typically: 089 12345678 -> 008912345678 -> strip 0 -> 8912345678
        // CC: 8 invalid, 89 invalid, 891 invalid -> rejected. Hmm.
        // Actually for 10-digit German: 08912345678 -> strip 0 -> 8912345678 -> CC: 8 nope, 89 nope -> invalid
        // This IS a limitation for countries where stripped trunk digits don't match any CC.
        // German numbers NEED +49 prefix or 0049 IDD to normalize correctly.
        assert_phone_invalid("0891234567");
    }

    #[test]
    fn phone_trunk_prefix_too_short() {
        // 0 + 8 digits = 9 digits total, below the 10-digit minimum for trunk stripping
        assert_phone_invalid("012345678");
    }

    #[test]
    fn phone_cross_format_uk_local_to_local() {
        // Two UK local formats normalize consistently (both strip trunk 0)
        assert_phone("020 7946 0958", "+2079460958");
        assert_phone("02079460958", "+2079460958");
    }

    #[test]
    fn phone_cross_format_french_local_to_local() {
        // Two French local formats normalize consistently
        assert_phone("06 12 34 56 78", "+612345678");
        assert_phone("0612345678", "+612345678");
    }

    #[test]
    fn phone_invalid_bad_country_code() {
        // +0 is never valid (no country code starts with 0)
        assert_phone_invalid("+0123456789");
    }

    // -- Country code validation --

    #[test]
    fn country_code_lookup_1digit() {
        assert!(is_valid_country_code(1));  // US/NANP
        assert!(is_valid_country_code(7));  // Russia
        assert!(!is_valid_country_code(2)); // Not a valid 1-digit code
        assert!(!is_valid_country_code(9)); // Not a valid 1-digit code
    }

    #[test]
    fn country_code_lookup_2digit() {
        assert!(is_valid_country_code(44)); // UK
        assert!(is_valid_country_code(91)); // India
        assert!(is_valid_country_code(86)); // China
        assert!(!is_valid_country_code(99)); // Not assigned
        assert!(!is_valid_country_code(35)); // Not a 2-digit code (350+ are)
    }

    #[test]
    fn country_code_lookup_3digit() {
        assert!(is_valid_country_code(353)); // Ireland
        assert!(is_valid_country_code(234)); // Nigeria
        assert!(is_valid_country_code(852)); // Hong Kong
        assert!(!is_valid_country_code(999)); // Not assigned
    }

    #[test]
    fn extract_cc_prefix_free() {
        // E.164 codes are prefix-free: shortest valid match is always correct
        assert_eq!(extract_country_code("15551234567"), Some((1, 1)));
        assert_eq!(extract_country_code("442079460958"), Some((44, 2)));
        assert_eq!(extract_country_code("353871234567"), Some((353, 3)));
        assert_eq!(extract_country_code("8613800138000"), Some((86, 2)));
        assert_eq!(extract_country_code("919876543210"), Some((91, 2)));
    }

    // =========================================================================
    // Barcode / GTIN normalizer tests
    // =========================================================================

    fn assert_barcode(input: &str, expected: &str) {
        match BarcodeNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                assert_eq!(result, expected, "Input: {:?}", input);
            }
            NormalizationResult::Invalid(reason) => {
                panic!("Expected {:?} for {:?}, got Invalid({:?})", expected, input, reason);
            }
        }
    }

    fn assert_barcode_invalid(input: &str) {
        match BarcodeNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                panic!("Expected Invalid for {:?}, got Normalized({:?})", input, result);
            }
            NormalizationResult::Invalid(_) => {}
        }
    }

    #[test]
    fn barcode_ean13_valid() {
        // Standard EAN-13: 4006381333931 (check digit 1)
        assert_barcode("4006381333931", "4006381333931");
    }

    #[test]
    fn barcode_ean13_with_hyphens() {
        assert_barcode("4-006381-333931", "4006381333931");
    }

    #[test]
    fn barcode_ean13_with_spaces() {
        assert_barcode("4006381 333931", "4006381333931");
    }

    #[test]
    fn barcode_ean13_invalid_check() {
        assert_barcode_invalid("4006381333932"); // wrong check digit
    }

    #[test]
    fn barcode_upca_valid() {
        // UPC-A: 036000291452 (check digit 2)
        // Should convert to EAN-13 by prepending 0
        assert_barcode("036000291452", "0036000291452");
    }

    #[test]
    fn barcode_upca_with_hyphens() {
        assert_barcode("0-36000-29145-2", "0036000291452");
    }

    #[test]
    fn barcode_upca_invalid_check() {
        assert_barcode_invalid("036000291453"); // wrong check digit
    }

    #[test]
    fn barcode_gtin8_valid() {
        // GTIN-8: 96385074 (check digit 4)
        assert_barcode("96385074", "0000096385074");
    }

    #[test]
    fn barcode_gtin8_invalid_check() {
        assert_barcode_invalid("96385075"); // wrong check digit
    }

    #[test]
    fn barcode_gtin14_valid() {
        // GTIN-14: 10614141000415 -> strip indicator "1", take inner 12 digits,
        // recalculate EAN-13 check digit for cross-format matching
        assert_barcode("10614141000415", "0614141000418");
    }

    #[test]
    fn barcode_gtin14_invalid_check() {
        assert_barcode_invalid("10614141000416");
    }

    #[test]
    fn barcode_isbn10_to_isbn13() {
        // ISBN-10: 0306406152 -> ISBN-13: 9780306406157
        assert_barcode("0306406152", "9780306406157");
    }

    #[test]
    fn barcode_cross_format_upca_ean13() {
        // UPC-A 036000291452 and EAN-13 0036000291452 should normalize to same value
        let upca = BarcodeNormalizer.normalize("036000291452");
        let ean13 = BarcodeNormalizer.normalize("0036000291452");
        match (upca, ean13) {
            (NormalizationResult::Normalized(a), NormalizationResult::Normalized(b)) => {
                assert_eq!(a, b);
            }
            _ => panic!("Expected both to normalize successfully"),
        }
    }

    #[test]
    fn barcode_invalid_empty() {
        assert_barcode_invalid("");
    }

    #[test]
    fn barcode_invalid_too_short() {
        assert_barcode_invalid("12345"); // 5 digits - not a valid barcode length
    }

    #[test]
    fn barcode_invalid_no_digits() {
        assert_barcode_invalid("ABCDEFGH");
    }

    #[test]
    fn barcode_idempotent() {
        // Normalizing an already-normalized EAN-13 should return same value
        assert_barcode("4006381333931", "4006381333931");
    }

    // =========================================================================
    // MPN normalizer tests
    // =========================================================================

    fn assert_mpn(input: &str, expected: &str) {
        match MpnNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                assert_eq!(result, expected, "Input: {:?}", input);
            }
            NormalizationResult::Invalid(reason) => {
                panic!("Expected {:?} for {:?}, got Invalid({:?})", expected, input, reason);
            }
        }
    }

    #[test]
    fn mpn_uppercase() {
        assert_mpn("abc-123", "ABC123");
    }

    #[test]
    fn mpn_strip_hyphens() {
        assert_mpn("MK-7500-B", "MK7500B");
    }

    #[test]
    fn mpn_strip_dots() {
        assert_mpn("A.B.C.123", "ABC123");
    }

    #[test]
    fn mpn_strip_spaces() {
        assert_mpn("MK 7500 B", "MK7500B");
    }

    #[test]
    fn mpn_strip_underscores() {
        assert_mpn("MK_7500_B", "MK7500B");
    }

    #[test]
    fn mpn_trim_leading_zeros() {
        assert_mpn("00123ABC", "123ABC");
    }

    #[test]
    fn mpn_all_zeros() {
        assert_mpn("000", "0");
    }

    #[test]
    fn mpn_cross_format() {
        // Same MPN, different formatting -> same output
        let formats = ["MK-7500-B", "mk7500b", "MK 7500 B", "MK_7500_B", "mk-7500-b"];
        for fmt in &formats {
            assert_mpn(fmt, "MK7500B");
        }
    }

    #[test]
    fn mpn_empty() {
        match MpnNormalizer.normalize("") {
            NormalizationResult::Invalid(_) => {}
            NormalizationResult::Normalized(s) => {
                panic!("Expected Invalid for empty, got {:?}", s);
            }
        }
    }

    #[test]
    fn mpn_whitespace_only() {
        match MpnNormalizer.normalize("   ") {
            NormalizationResult::Invalid(_) => {}
            NormalizationResult::Normalized(s) => {
                panic!("Expected Invalid for whitespace, got {:?}", s);
            }
        }
    }

    // =========================================================================
    // Brand normalizer tests
    // =========================================================================

    fn assert_brand(input: &str, expected: &str) {
        match BrandNormalizer.normalize(input) {
            NormalizationResult::Normalized(result) => {
                assert_eq!(result, expected, "Input: {:?}", input);
            }
            NormalizationResult::Invalid(reason) => {
                panic!("Expected {:?} for {:?}, got Invalid({:?})", expected, input, reason);
            }
        }
    }

    #[test]
    fn brand_lowercase() {
        assert_brand("Samsung", "samsung");
    }

    #[test]
    fn brand_strip_inc() {
        assert_brand("Apple Inc", "apple");
    }

    #[test]
    fn brand_strip_inc_dot() {
        assert_brand("Apple Inc.", "apple");
    }

    #[test]
    fn brand_strip_llc() {
        assert_brand("Tesla LLC", "tesla");
    }

    #[test]
    fn brand_strip_ltd() {
        assert_brand("Sony Ltd", "sony");
    }

    #[test]
    fn brand_strip_corporation() {
        assert_brand("Microsoft Corporation", "microsoft");
    }

    #[test]
    fn brand_strip_comma_inc() {
        assert_brand("Apple, Inc.", "apple");
    }

    #[test]
    fn brand_strip_gmbh() {
        assert_brand("Siemens GmbH", "siemens");
    }

    #[test]
    fn brand_strip_the() {
        assert_brand("The North Face", "north face");
    }

    #[test]
    fn brand_normalize_ampersand() {
        assert_brand("Procter & Gamble", "procter and gamble");
    }

    #[test]
    fn brand_cross_format() {
        let formats = [
            "Apple Inc.",
            "Apple, Inc.",
            "apple inc",
            "APPLE INC.",
            "Apple",
            "apple",
        ];
        for fmt in &formats {
            assert_brand(fmt, "apple");
        }
    }

    #[test]
    fn brand_empty() {
        match BrandNormalizer.normalize("") {
            NormalizationResult::Invalid(_) => {}
            NormalizationResult::Normalized(s) => {
                panic!("Expected Invalid for empty, got {:?}", s);
            }
        }
    }

    #[test]
    fn brand_preserves_content() {
        // Should not strip brand-like suffixes from the middle
        assert_brand("Company Store", "company store");
    }
}
