use std::borrow::Cow;

use cow_utils::CowUtils;
use oxc_ast::{AstKind, ast::NumericLiteral};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{AstNode, context::LintContext, rule::Rule};

fn no_loss_of_precision_diagnostic(span: Span) -> OxcDiagnostic {
    OxcDiagnostic::warn("This number literal will lose precision at runtime.")
        .with_help(
            "Use a number literal representable by a 64-bit floating-point number, or use a `BigInt` literal (for example, `9007199254740993n`) for exact large integers.",
        )
        .with_note(
            "In JavaScript, `Number` values exactly represent integers only in the range -9007199254740991 to 9007199254740991 (`Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). `BigInt` supports arbitrarily large integers.",
        )
        .with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct NoLossOfPrecision;

declare_oxc_lint!(
    /// ### What it does
    ///
    /// Disallow precision loss in numeric literals.
    ///
    /// ### Why is this bad?
    ///
    /// It can lead to unexpected results in certain situations.
    /// For example, when performing mathematical operations.
    ///
    /// In JavaScript, Numbers are stored as double-precision floating-point numbers
    /// according to the IEEE 754 standard. Because of this, numbers can only
    /// retain accuracy up to a certain amount of digits. If the programmer
    /// enters additional digits, those digits will be lost in the conversion
    /// to the Number type and will result in unexpected/incorrect behavior.
    ///
    /// ### Examples
    ///
    /// Examples of **incorrect** code for this rule:
    /// ```javascript
    /// var x = 2e999;
    /// ```
    ///
    /// ```javascript
    /// var x = 9007199254740993;
    /// ```
    ///
    /// ```javascript
    /// var x = 5123000000000000000000000000001;
    /// ```
    ///
    /// ```javascript
    /// var x = 1230000000000000000000000.0;
    /// ```
    ///
    /// ```javascript
    /// var x = 0X200000_0000000_1;
    /// ```
    ///
    /// Examples of **correct** code for this rule:
    /// ```javascript
    /// var x = 12345;
    /// ```
    ///
    /// ```javascript
    /// var x = 123.456;
    /// ```
    ///
    /// ```javascript
    /// var x = 123.0000000000000000000000;
    /// ```
    ///
    /// ```javascript
    /// var x = 123e34;
    /// ```
    ///
    /// ```javascript
    /// var x = 0x1FFF_FFFF_FFF_FFF;
    /// ```
    NoLossOfPrecision,
    eslint,
    correctness,
    version = "0.0.7",
    short_description = "Disallow precision loss in numeric literals.",
);

impl Rule for NoLossOfPrecision {
    fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
        match node.kind() {
            AstKind::NumericLiteral(node) if Self::lose_precision(node) => {
                ctx.diagnostic(no_loss_of_precision_diagnostic(node.span));
            }
            _ => {}
        }
    }
}

pub struct RawNum<'a> {
    int: &'a str,
    frac: &'a str,
    exp: isize,
}

#[derive(Debug)]
pub struct ScientificNotation<'a> {
    int: &'a str,
    frac: Cow<'a, str>,
    exp: isize,
}

impl PartialEq for ScientificNotation<'_> {
    fn eq(&self, other: &Self) -> bool {
        if self.int == other.int && self.frac == other.frac {
            if self.int == "0" && self.frac.is_empty() {
                return true;
            }
            return self.exp == other.exp;
        }
        false
    }
}

impl<'a> RawNum<'a> {
    fn new(num: &str) -> Option<RawNum<'_>> {
        // remove sign
        let num = num.trim_start_matches(['+', '-']);

        let (int, num_without_int) = {
            // skip leading zeros
            let num_without_zeros = num.trim_start_matches('0');

            // read the integer part and store the end index
            let int_end = num_without_zeros
                .chars()
                .position(|ch| !ch.is_ascii_digit())
                .unwrap_or(num_without_zeros.len());

            // if no integer part was found, default to 0
            let int = if int_end == 0 { "0" } else { &num_without_zeros[..int_end] };

            // make a slice of the rest of the string
            let num_without_int = &num_without_zeros[int_end..];

            (int, num_without_int)
        };

        // if next char is a dot, parse the fractional part
        let (frac, num_without_frac) =
            num_without_int.strip_prefix('.').map_or(("", num_without_int), |num_without_dot| {
                let frac_end = num_without_dot
                    .chars()
                    .position(|ch| !ch.is_ascii_digit())
                    .unwrap_or(num_without_dot.len());

                // slice the fractional part and the rest of the string
                let frac = &num_without_dot[..frac_end];
                let num_without_frac = &num_without_dot[frac_end..];

                (frac, num_without_frac)
            });

        // if next char is an e, treat the rest as the exponent
        let exp =
            num_without_frac.strip_prefix(['e', 'E']).map_or("0", |num_without_e| num_without_e);

        let Ok(exp) = exp.parse::<isize>() else {
            return None;
        };

        Some(RawNum { int, frac, exp })
    }

    fn normalize(&mut self, parse_as_float: bool) -> ScientificNotation<'a> {
        if self.int == "0" && !self.frac.is_empty() {
            let frac_zeros = self.frac.chars().take_while(|&ch| ch == '0').count();
            #[expect(clippy::cast_possible_wrap)]
            let exp = self.exp - 1 - frac_zeros as isize;
            self.frac = &self.frac[frac_zeros..];

            match self.frac.len() {
                0 => ScientificNotation { int: "0", frac: Cow::Borrowed(""), exp },
                1 => ScientificNotation { int: &self.frac[..1], frac: Cow::Borrowed(""), exp },
                _ => ScientificNotation {
                    int: &self.frac[..1],
                    frac: Cow::Borrowed(&self.frac[1..]),
                    exp,
                },
            }
        } else {
            #[expect(clippy::cast_possible_wrap)]
            let exp = self.exp + self.int.len() as isize - 1;
            if self.int.len() == 1 {
                ScientificNotation { int: self.int, frac: Cow::Borrowed(self.frac), exp }
            } else {
                let frac = if self.frac.is_empty() {
                    if parse_as_float {
                        Cow::Borrowed(&self.int[1..])
                    } else {
                        let int_trimmed = self.int.trim_end_matches('0');
                        if int_trimmed.len() == 1 {
                            Cow::Borrowed("")
                        } else {
                            Cow::Borrowed(&int_trimmed[1..])
                        }
                    }
                } else {
                    Cow::Owned(format!("{}{}", &self.int[1..], self.frac))
                };

                ScientificNotation { int: &self.int[..1], frac, exp }
            }
        }
    }
}

impl NoLossOfPrecision {
    const MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS: usize = 15;

    fn strip_numeric_separators(raw: &str) -> Cow<'_, str> {
        if !raw.as_bytes().contains(&b'_') {
            return Cow::Borrowed(raw);
        }
        raw.cow_replace('_', "")
    }

    fn non_base_ten_literal_is_exact(raw: &str) -> bool {
        let raw = raw.trim_start_matches(['+', '-']);
        let (radix, digits) =
            if let Some(digits) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) {
                (2, digits)
            } else if let Some(digits) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) {
                (8, digits)
            } else if let Some(digits) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) {
                (16, digits)
            } else {
                (8, raw.trim_start_matches('0'))
            };

        let mut bit_len = 0;
        for byte in digits.bytes() {
            if byte == b'_' {
                continue;
            }

            let digit = match byte {
                b'0'..=b'9' => byte - b'0',
                b'a'..=b'f' => byte - b'a' + 10,
                b'A'..=b'F' => byte - b'A' + 10,
                _ => return false,
            };

            if bit_len == 0 {
                if digit == 0 {
                    continue;
                }
                bit_len = match radix {
                    2 => 1,
                    8 => match digit {
                        1 => 1,
                        2 | 3 => 2,
                        _ => 3,
                    },
                    _ => match digit {
                        1 => 1,
                        2 | 3 => 2,
                        4..=7 => 3,
                        _ => 4,
                    },
                };
            } else {
                bit_len += match radix {
                    2 => 1,
                    8 => 3,
                    _ => 4,
                };
            }

            if bit_len > 53 {
                return false;
            }
        }

        true
    }

    fn base_ten_literal_is_safe(raw: &str, value: f64) -> bool {
        if !value.is_finite() {
            return false;
        }

        let raw = raw.trim_start_matches(['+', '-']);
        let exponent_start = raw.find(['e', 'E']).unwrap_or(raw.len());
        if exponent_start != raw.len() {
            return false;
        }
        let mantissa = &raw[..exponent_start];

        let mut digit_index = 0;
        let mut first_non_zero = None;
        let mut last_non_zero = None;
        let mut fractional_has_non_zero = false;
        let mut dot_digit_index = None;

        for byte in mantissa.bytes() {
            match byte {
                b'_' => continue,
                b'.' => {
                    dot_digit_index = Some(digit_index);
                    continue;
                }
                b'0'..=b'9' => {}
                _ => return false,
            }

            if byte != b'0' {
                first_non_zero.get_or_insert(digit_index);
                last_non_zero = Some(digit_index);
                fractional_has_non_zero |= dot_digit_index.is_some();
            }
            digit_index += 1;
        }

        let Some(first_non_zero) = first_non_zero else {
            return true;
        };
        if value == 0.0 {
            return false;
        }

        if let Some(dot_digit_index) = dot_digit_index
            && !fractional_has_non_zero
        {
            return dot_digit_index - first_non_zero <= Self::MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS;
        }

        let last_significant =
            if dot_digit_index.is_some() { digit_index - 1 } else { last_non_zero.unwrap() };
        last_significant - first_non_zero < Self::MAX_OBVIOUSLY_SAFE_SIGNIFICANT_DIGITS
    }

    fn not_base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
        let raw = node.raw.as_ref().unwrap().as_str();
        if Self::non_base_ten_literal_is_exact(raw) {
            return false;
        }

        let raw = Self::strip_numeric_separators(raw);
        let raw = raw.cow_to_ascii_uppercase();
        #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
        // AST always store number as f64, need a cast to format in bin/oct/hex
        let value = node.value as u64;
        let suffix = if raw.starts_with("0B") {
            format!("{value:b}")
        } else if raw.starts_with("0X") {
            format!("{value:x}")
        } else {
            format!("{value:o}")
        };
        !raw.ends_with(&suffix.cow_to_ascii_uppercase().as_ref())
    }

    fn base_ten_loses_precision(node: &'_ NumericLiteral) -> bool {
        let raw = node.raw.as_ref().unwrap().as_str();
        if Self::base_ten_literal_is_safe(raw, node.value) {
            return false;
        }

        let raw = Self::strip_numeric_separators(raw);
        let Some(raw) = Self::normalize(&raw, false) else {
            return true;
        };

        let total_significant_digits = raw.int.len() + raw.frac.len();

        if total_significant_digits > 100 {
            return true;
        }

        let stored = to_precision(node.value, total_significant_digits);

        let Some(stored) = Self::normalize(&stored, true) else {
            return true;
        };
        raw != stored
    }

    fn normalize(num: &str, parse_as_float: bool) -> Option<ScientificNotation<'_>> {
        let coefficient = num.trim_start_matches(['+', '-']);
        let exp_start =
            coefficient.find('e').or_else(|| coefficient.find('E')).unwrap_or(coefficient.len());
        let parse_as_float = parse_as_float || coefficient[..exp_start].contains('.');
        Some(RawNum::new(num)?.normalize(parse_as_float))
    }

    pub fn lose_precision(node: &'_ NumericLiteral) -> bool {
        if node.base.is_base_10() {
            Self::base_ten_loses_precision(node)
        } else {
            Self::not_base_ten_loses_precision(node)
        }
    }
}

/// `flt_str_to_exp` - used in `to_precision`
///
/// This function traverses a string representing a number,
/// returning the floored log10 of this number.
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_possible_wrap)]
fn flt_str_to_exp(flt: &str) -> i32 {
    let mut non_zero_encountered = false;
    let mut dot_encountered = false;
    for (i, c) in flt.char_indices() {
        if c == '.' {
            if non_zero_encountered {
                return (i as i32) - 1;
            }
            dot_encountered = true;
        } else if c != '0' {
            if dot_encountered {
                return 1 - (i as i32);
            }
            non_zero_encountered = true;
        }
    }
    (flt.len() as i32) - 1
}

/// `round_to_precision` - used in `to_precision`
///
/// This procedure has two roles:
/// - If there are enough or more than enough digits in the
///   string to show the required precision, the number
///   represented by these digits is rounded using string
///   manipulation.
/// - Else, zeroes are appended to the string.
/// - Additionally, sometimes the exponent was wrongly computed and
///   while up-rounding we find that we need an extra digit. When this
///   happens, we return true so that the calling context can adjust
///   the exponent. The string is kept at an exact length of `precision`.
///
/// When this procedure returns, `digits` is exactly `precision` long.
fn round_to_precision(digits: &mut String, precision: usize) -> bool {
    if digits.len() > precision {
        let to_round = digits.split_off(precision);
        let mut digit =
            digits.pop().expect("already checked that length is bigger than precision") as u8;
        if let Some(first) = to_round.chars().next()
            && first > '4'
        {
            digit += 1;
        }

        if digit as char == ':' {
            // ':' is '9' + 1
            // need to propagate the increment backward
            let mut replacement = String::from("0");
            let mut propagated = false;
            for c in digits.chars().rev() {
                let d = match (c, propagated) {
                    ('0'..='8', false) => (c as u8 + 1) as char,
                    (_, false) => '0',
                    (_, true) => c,
                };
                replacement.push(d);
                if d != '0' {
                    propagated = true;
                }
            }
            digits.clear();
            let replacement = if propagated {
                replacement.as_str()
            } else {
                digits.push('1');
                &replacement.as_str()[1..]
            };
            for c in replacement.chars().rev() {
                digits.push(c);
            }
            !propagated
        } else {
            digits.push(digit as char);
            false
        }
    } else {
        digits.push_str(&"0".repeat(precision - digits.len()));
        false
    }
}

fn fractional_digits_for_precision(num: f64, precision: usize) -> usize {
    let exponent = format!("{num:e}")
        .rsplit_once('e')
        .and_then(|(_, exp)| exp.parse::<isize>().ok())
        .unwrap_or_default();

    if exponent.is_negative() {
        exponent.unsigned_abs().saturating_add(precision).saturating_add(1).max(100)
    } else {
        100
    }
}

/// Mimics JavaScript's `Number.prototype.toPrecision()` method
///
/// The `toPrecision()` method returns a string representing the Number object to the specified precision.
///
/// More information:
///  - [ECMAScript reference][spec]
///  - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision
#[expect(clippy::cast_possible_truncation)]
#[expect(clippy::cast_possible_wrap)]
#[expect(clippy::cast_sign_loss)]
pub fn to_precision(mut num: f64, precision: usize) -> String {
    // Validate precision range (1-100)
    debug_assert!((1..=100).contains(&precision), "Precision must be between 1 and 100");

    // Handle non-finite numbers
    if !num.is_finite() {
        if num.is_nan() {
            return "NaN".to_string();
        } else if num.is_infinite() {
            return if num.is_sign_positive() { "Infinity" } else { "-Infinity" }.to_string();
        }
    }

    let precision_i32 = precision as i32;

    // Handle sign
    let mut prefix = String::new();
    if num < 0.0 {
        prefix.push('-');
        num = -num;
    }

    let mut suffix: String;
    let mut exponent: i32;

    // Handle zero
    if num == 0.0 {
        suffix = "0".repeat(precision);
        exponent = 0;
    } else {
        // For very small numbers (e.g. 3e-308), we need more than 100 fractional digits
        // to keep enough significant digits for precision comparison.
        let fractional_digits = fractional_digits_for_precision(num, precision);
        suffix = format!("{num:.fractional_digits$}");

        // Calculate exponent
        exponent = flt_str_to_exp(&suffix);

        // Extract relevant digits only
        if exponent < 0 {
            suffix = suffix.split_off((1 - exponent) as usize);
        } else if let Some(n) = suffix.find('.') {
            suffix.remove(n);
        }

        // Round to the specified precision
        if round_to_precision(&mut suffix, precision) {
            exponent += 1;
        }

        // Decide between scientific and fixed notation
        let great_exp = exponent >= precision_i32;
        if exponent < -6 || great_exp {
            // Use scientific notation
            if precision > 1 {
                suffix.insert(1, '.');
            }
            suffix.push('e');
            if great_exp {
                suffix.push('+');
            }
            suffix.push_str(&exponent.to_string());

            return prefix + &suffix;
        }
    }

    // Use fixed-point notation
    let e_inc = exponent + 1;
    if e_inc == precision_i32 {
        return prefix + &suffix;
    }

    if exponent >= 0 {
        suffix.insert(e_inc as usize, '.');
    } else {
        prefix.push('0');
        prefix.push('.');
        prefix.push_str(&"0".repeat(-e_inc as usize));
    }

    prefix + &suffix
}

#[test]
fn test() {
    use crate::tester::Tester;

    let pass = vec![
        "var x = 12345",
        "var x = 123.456",
        "var x = -123.456",
        "var x = -123456",
        "var x = 123e34",
        "var x = 123.0e34",
        "var x = 123e-34",
        "var x = -123e34",
        "var x = -123e-34",
        "var x = 12.3e34",
        "var x = 12.3e-34",
        "var x = -12.3e34",
        "var x = -12.3e-34",
        "var x = 12300000000000000000000000",
        "var x = -12300000000000000000000000",
        "var x = 0.00000000000000000000000123",
        "var x = -0.00000000000000000000000123",
        "var x = 9007199254740991",
        "var x = 0",
        "var x = 0.0",
        "var x = 0.000000000000000000000000000000000000000000000000000000000000000000000000000000",
        "var x = -0",
        "var x = 123.0000000000000000000000",
        "var x = 9.00e2",
        "var x = 9.000e3",
        "var x = 9.0000000000e10",
        "var x = 9.00E2",
        "var x = 9.000E3",
        "var x = 9.100E3",
        "var x = 9.0000000000E10",
        "var x = 019.5",
        "var x = 0195",
        "var x = 00195",
        "var x = 0008",
        "var x = 0e5",
        "var x = .42",
        "var x = 42.",
        "var x = 12_34_56",                          // { "ecmaVersion": 2021 },
        "var x = 12_3.4_56",                         // { "ecmaVersion": 2021 },
        "var x = -12_3.4_56",                        // { "ecmaVersion": 2021 },
        "var x = -12_34_56",                         // { "ecmaVersion": 2021 },
        "var x = 12_3e3_4",                          // { "ecmaVersion": 2021 },
        "var x = 123.0e3_4",                         // { "ecmaVersion": 2021 },
        "var x = 12_3e-3_4",                         // { "ecmaVersion": 2021 },
        "var x = 12_3.0e-3_4",                       // { "ecmaVersion": 2021 },
        "var x = -1_23e-3_4",                        // { "ecmaVersion": 2021 },
        "var x = -1_23.8e-3_4",                      // { "ecmaVersion": 2021 },
        "var x = 1_230000000_00000000_00000_000",    // { "ecmaVersion": 2021 },
        "var x = -1_230000000_00000000_00000_000",   // { "ecmaVersion": 2021 },
        "var x = 0.0_00_000000000_000000000_00123",  // { "ecmaVersion": 2021 },
        "var x = -0.0_00_000000000_000000000_00123", // { "ecmaVersion": 2021 },
        "var x = 0e5_3",                             // { "ecmaVersion": 2021 },
        "var x = 0b11111111111111111111111111111111111111111111111111111", // { "ecmaVersion": 6 },
        "var x = 0b111_111_111_111_1111_11111_111_11111_1111111111_11111111_111_111", // { "ecmaVersion": 2021 },
        "var x = 0B11111111111111111111111111111111111111111111111111111", // { "ecmaVersion": 6 },
        "var x = 0B111_111_111_111_1111_11111_111_11111_1111111111_11111111_111_111", // { "ecmaVersion": 2021 },
        "var x = 0o377777777777777777",       // { "ecmaVersion": 6 },
        "var x = 0o3_77_777_777_777_777_777", // { "ecmaVersion": 2021 },
        "var x = 0O377777777777777777",       // { "ecmaVersion": 6 },
        // "var x = 0377777777777777777", // '0'-prefixed octal literals and octal escape sequences are deprecated
        "var x = 0x1FFFFFFFFFFFFF",
        "var x = 0X1FFFFFFFFFFFFF",
        "var x = true",
        "var x = 'abc'",
        "var x = ''",
        "var x = null",
        "var x = undefined",
        "var x = {}",
        "var x = ['a', 'b']",
        "var x = new Date()",
        "var x = '9007199254740993'",
        "var x = 0x1FFF_FFFF_FFF_FFF",  // { "ecmaVersion": 2021 },
        "var x = 0X1_FFF_FFFF_FFF_FFF", // { "ecmaVersion": 2021 },
        "const x = 12345;",
        "const x = 123.456;",
        "const x = -123.456;",
        "const x = 123_456;",
        "const x = 123_00_000_000_000_000_000_000_000;",
        "const x = 123.000_000_000_000_000_000_000_0;",
        "var a = Infinity",
        "var a = 480.00",
        "var a = -30.00",
        "let a = Infinity",
        "let a = 480.00",
        "let a = -30.00",
        "const a = Infinity",
        "const a = 480.00",
        "const a = -30.00",
        "const x = 3e-308",
        "(1000000000000000128).toFixed(0)",
    ];

    let fail = vec![
        "var x = 9007199254740993",
        "var x = 9007199254740.993e3",
        "var x = 9.007199254740993e15",
        "var x = -9007199254740993",
        "var x = 900719.9254740994",
        "var x = -900719.9254740994",
        "const x = -9_00719_9254_740993",
        "const x = -900_719.92_5474_0994",
        "const x = 9.0_0719925_474099_3e15",
        "const x = 900_719.92_54740_994",
        "let x = -9_00719_9254_740993",
        "let x = -900_719.92_5474_0994",
        "let x = 9.0_0719925_474099_3e15",
        "let x = 900_719.92_54740_994",
        "var x = -9_00719_9254_740993",  // { "ecmaVersion": 2021 },
        "var x = -900_719.92_5474_0994", // { "ecmaVersion": 2021 },
        "var x = .9007199254740993e16",
        "var x = 9.0_0719925_474099_3e15", // { "ecmaVersion": 2021 },
        "var x = 90_0719925_4740.9_93e3",  // { "ecmaVersion": 2021 },
        "var x = 900_719.92_54740_994",    // { "ecmaVersion": 2021 },
        "var x = 900719925474099_3",       // { "ecmaVersion": 2021 },
        "var x = 900719925474099.30e1",
        "var x = 90071992547409930e-1",
        "var x = 5123000000000000000000000000001",
        "var x = -5123000000000000000000000000001",
        "var x = 1230000000000000000000000.0",
        "var x = 1.0000000000000000000000123",
        "var x = 17498005798264095394980017816940970922825355447145699491406164851279623993595007385788105416184430592",
        "var x = 2e999",
        "var x = .1230000000000000000000000",
        "var x = 0b100000000000000000000000000000000000000000000000000001", // { "ecmaVersion": 6 },
        "var x = 0B100000000000000000000000000000000000000000000000000001", // { "ecmaVersion": 6 },
        "var x = 0o400000000000000001",                                     // { "ecmaVersion": 6 },
        "var x = 0O400000000000000001",                                     // { "ecmaVersion": 6 },
        "var x = 0400000000000000001",
        "var x = 0x20000000000001",
        "var x = 0X20000000000001",
        "var x = 5123_00000000000000000000000000_1", // { "ecmaVersion": 2021 },
        "var x = -5_12300000000000000000000_0000001", // { "ecmaVersion": 2021 },
        "var x = 123_00000000000000000000_00.0_0",   // { "ecmaVersion": 2021 },
        "var x = 1.0_00000000000000000_0000123",     // { "ecmaVersion": 2021 },
        "var x = 174_980057982_640953949800178169_409709228253554471456994_914061648512796239935950073857881054_1618443059_2", // { "ecmaVersion": 2021 },
        "var x = 2e9_99",                         // { "ecmaVersion": 2021 },
        "var x = .1_23000000000000_00000_0000_0", // { "ecmaVersion": 2021 },
        "var x = 0b1_0000000000000000000000000000000000000000000000000000_1", // { "ecmaVersion": 2021 },
        "var x = 0B10000000000_0000000000000000000000000000_000000000000001", // { "ecmaVersion": 2021 },
        "var x = 0o4_00000000000000_001", // { "ecmaVersion": 2021 },
        "var x = 0O4_0000000000000000_1", // { "ecmaVersion": 2021 },
        "var x = 0x2_0000000000001",      // { "ecmaVersion": 2021 },
        "var x = 0X200000_0000000_1",     // { "ecmaVersion": 2021 },
        "const x = 9007199254740993;",
        "const x = 9_007_199_254_740_993;",
        "const x = 9_007_199_254_740.993e3;",
        "const x = 0b100_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000_001;",
        "var x = 1e18_446_744_073_709_551_615",
        "var x = 96215808661.52751e-84",
    ];

    Tester::new(NoLossOfPrecision::NAME, NoLossOfPrecision::PLUGIN, pass, fail).test_and_snapshot();
}
