use crate::product::tui_app::color::perceptual_distance;
use ratatui::style::Color;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;

static DEFAULT_PALETTE_VERSION: AtomicU64 = AtomicU64::new(0);

fn bump_palette_version() {
    DEFAULT_PALETTE_VERSION.fetch_add(1, Ordering::Relaxed);
}

/// Returns the closest color to the target color that the terminal can display.
pub fn best_color(target: (u8, u8, u8)) -> Color {
    let Some(color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else {
        return Color::default();
    };
    if color_level.has_16m {
        let (r, g, b) = target;
        #[allow(clippy::disallowed_methods)]
        Color::Rgb(r, g, b)
    } else if color_level.has_256
        && let Some((i, _)) = nearest_xterm_fixed_color(target)
    {
        #[allow(clippy::disallowed_methods)]
        Color::Indexed(i as u8)
    } else {
        #[allow(clippy::disallowed_methods)]
        Color::default()
    }
}

/// Returns the closest displayable color to `target` without choosing the same
/// 256-color bucket as `avoid`.
pub fn best_color_distinct_from(target: (u8, u8, u8), avoid: (u8, u8, u8)) -> Color {
    let Some(color_level) = supports_color::on_cached(supports_color::Stream::Stdout) else {
        return Color::default();
    };
    if color_level.has_16m {
        let (r, g, b) = target;
        #[allow(clippy::disallowed_methods)]
        Color::Rgb(r, g, b)
    } else if color_level.has_256
        && let Some((i, _)) = nearest_xterm_fixed_color_distinct_from(target, avoid)
    {
        #[allow(clippy::disallowed_methods)]
        Color::Indexed(i as u8)
    } else {
        #[allow(clippy::disallowed_methods)]
        Color::default()
    }
}

pub fn requery_default_colors() {
    imp::requery_default_colors();
    bump_palette_version();
}

#[derive(Clone, Copy)]
pub struct DefaultColors {
    fg: (u8, u8, u8),
    bg: (u8, u8, u8),
}

pub fn default_colors() -> Option<DefaultColors> {
    imp::default_colors()
}

pub fn default_fg() -> Option<(u8, u8, u8)> {
    default_colors().map(|c| c.fg)
}

pub fn default_bg() -> Option<(u8, u8, u8)> {
    default_colors().map(|c| c.bg)
}

/// Returns a monotonic counter that increments whenever `requery_default_colors()` runs
/// successfully so cached renderers can know when their styling assumptions (e.g.
/// background colors baked into cached transcript rows) are stale and need invalidation.
#[allow(dead_code)]
pub fn palette_version() -> u64 {
    DEFAULT_PALETTE_VERSION.load(Ordering::Relaxed)
}

fn parse_osc_color_response(buffer: &[u8], expected_slot: u8) -> Option<(u8, u8, u8)> {
    let mut index = 0;
    while index + 3 <= buffer.len() {
        let start = buffer[index..]
            .windows(2)
            .position(|window| window == b"\x1B]")?;
        index += start + 2;

        let content_end = find_osc_end(&buffer[index..])?;
        let content = std::str::from_utf8(&buffer[index..index + content_end]).ok()?;
        let mut parts = content.splitn(2, ';');
        let slot = parts.next()?.parse::<u8>().ok()?;
        let payload = parts.next()?;
        if slot == expected_slot
            && let Some(color) = parse_osc_color_payload(payload)
        {
            return Some(color);
        }
        index += content_end;
    }

    None
}

fn find_osc_end(buffer: &[u8]) -> Option<usize> {
    let mut index = 0;
    while index < buffer.len() {
        match buffer[index] {
            0x07 => return Some(index),
            0x1B if buffer.get(index + 1) == Some(&b'\\') => return Some(index),
            0x1B => return None,
            _ => {}
        }
        index += 1;
    }
    None
}

fn parse_osc_color_payload(payload: &str) -> Option<(u8, u8, u8)> {
    let (prefix, rest) = payload.split_once(':')?;
    let expected_components = match prefix {
        "rgb" => 3,
        "rgba" => 4,
        _ => return None,
    };
    let components: Vec<&str> = rest.split('/').collect();
    if components.len() != expected_components {
        return None;
    }

    Some((
        parse_osc_color_component(components[0])?,
        parse_osc_color_component(components[1])?,
        parse_osc_color_component(components[2])?,
    ))
}

fn parse_osc_color_component(component: &str) -> Option<u8> {
    if component.is_empty() || component.len() > 4 {
        return None;
    }
    let value = u32::from_str_radix(component, 16).ok()?;
    let max = (1u32 << (component.len() * 4)) - 1;
    Some(((value * 255 + max / 2) / max) as u8)
}

#[cfg(all(unix, not(test)))]
mod imp {
    use super::DefaultColors;
    use super::parse_osc_color_response;
    use std::fs::File;
    use std::fs::OpenOptions;
    use std::io::ErrorKind;
    use std::io::Read;
    use std::io::Write;
    use std::os::fd::AsRawFd;
    use std::sync::Mutex;
    use std::sync::OnceLock;
    use std::time::Duration;
    use std::time::Instant;

    const OSC_QUERY_TIMEOUT: Duration = Duration::from_millis(200);

    struct Cache<T> {
        attempted: bool,
        value: Option<T>,
    }

    impl<T> Default for Cache<T> {
        fn default() -> Self {
            Self {
                attempted: false,
                value: None,
            }
        }
    }

    impl<T: Copy> Cache<T> {
        fn get_or_init_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
            if !self.attempted {
                self.value = init();
                self.attempted = true;
            }
            self.value
        }

        fn refresh_with(&mut self, mut init: impl FnMut() -> Option<T>) -> Option<T> {
            self.value = init();
            self.attempted = true;
            self.value
        }
    }

    fn default_colors_cache() -> &'static Mutex<Cache<DefaultColors>> {
        static CACHE: OnceLock<Mutex<Cache<DefaultColors>>> = OnceLock::new();
        CACHE.get_or_init(|| Mutex::new(Cache::default()))
    }

    pub(super) fn default_colors() -> Option<DefaultColors> {
        let cache = default_colors_cache();
        let mut cache = cache.lock().ok()?;
        cache.get_or_init_with(|| query_default_colors().unwrap_or_default())
    }

    pub(super) fn requery_default_colors() {
        if let Ok(mut cache) = default_colors_cache().lock() {
            cache.refresh_with(|| query_default_colors().unwrap_or_default());
        }
    }

    fn query_default_colors() -> std::io::Result<Option<DefaultColors>> {
        let fg = query_color_slot(10)?;
        let bg = query_color_slot(11)?;
        Ok(fg.zip(bg).map(|(fg, bg)| DefaultColors { fg, bg }))
    }

    fn query_color_slot(slot: u8) -> std::io::Result<Option<(u8, u8, u8)>> {
        match OpenOptions::new().read(true).write(true).open("/dev/tty") {
            Ok(mut tty) => query_color_slot_from_tty(&mut tty, slot),
            Err(_) => {
                let sequence = format!("\x1B]{slot};?\x1B\\");
                std::io::stdout().write_all(sequence.as_bytes())?;
                std::io::stdout().flush()?;
                Ok(None)
            }
        }
    }

    fn query_color_slot_from_tty(
        tty: &mut File,
        slot: u8,
    ) -> std::io::Result<Option<(u8, u8, u8)>> {
        let _guard = NonblockingGuard::new(tty)?;
        let sequence = format!("\x1B]{slot};?\x1B\\");
        tty.write_all(sequence.as_bytes())?;
        tty.flush()?;

        let mut response = Vec::new();
        let deadline = Instant::now() + OSC_QUERY_TIMEOUT;
        let mut chunk = [0u8; 256];
        while Instant::now() < deadline {
            match tty.read(&mut chunk) {
                Ok(0) => std::thread::sleep(Duration::from_millis(5)),
                Ok(n) => {
                    response.extend_from_slice(&chunk[..n]);
                    if let Some(color) = parse_osc_color_response(&response, slot) {
                        return Ok(Some(color));
                    }
                }
                Err(err) if err.kind() == ErrorKind::WouldBlock => {
                    std::thread::sleep(Duration::from_millis(5));
                }
                Err(err) if err.kind() == ErrorKind::Interrupted => {}
                Err(err) => return Err(err),
            }
        }

        Ok(None)
    }

    struct NonblockingGuard {
        fd: i32,
        flags: i32,
    }

    impl NonblockingGuard {
        fn new(file: &File) -> std::io::Result<Self> {
            let fd = file.as_raw_fd();
            // SAFETY: fcntl only reads and updates status flags for a valid file descriptor.
            let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
            if flags < 0 {
                return Err(std::io::Error::last_os_error());
            }
            // SAFETY: fd is valid and flags are the previously read descriptor flags plus O_NONBLOCK.
            let updated = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
            if updated < 0 {
                return Err(std::io::Error::last_os_error());
            }
            Ok(Self { fd, flags })
        }
    }

    impl Drop for NonblockingGuard {
        fn drop(&mut self) {
            // SAFETY: restores the original descriptor flags for the same live file descriptor.
            let _ = unsafe { libc::fcntl(self.fd, libc::F_SETFL, self.flags) };
        }
    }
}

#[cfg(not(all(unix, not(test))))]
mod imp {
    use super::DefaultColors;

    pub(super) fn default_colors() -> Option<DefaultColors> {
        None
    }

    pub(super) fn requery_default_colors() {}
}

/// The subset of Xterm colors that are usually consistent across terminals.
fn xterm_fixed_colors() -> impl Iterator<Item = (usize, (u8, u8, u8))> {
    XTERM_COLORS.into_iter().enumerate().skip(16)
}

pub(crate) fn nearest_xterm_fixed_color(target: (u8, u8, u8)) -> Option<(usize, (u8, u8, u8))> {
    nearest_xterm_fixed_color_from(target, xterm_fixed_colors())
}

pub(crate) fn nearest_xterm_fixed_color_distinct_from(
    target: (u8, u8, u8),
    avoid: (u8, u8, u8),
) -> Option<(usize, (u8, u8, u8))> {
    let (avoid_idx, _) = nearest_xterm_fixed_color(avoid)?;
    nearest_xterm_fixed_color_from(
        target,
        xterm_fixed_colors().filter(|(idx, _)| *idx != avoid_idx),
    )
}

fn nearest_xterm_fixed_color_from(
    target: (u8, u8, u8),
    colors: impl Iterator<Item = (usize, (u8, u8, u8))>,
) -> Option<(usize, (u8, u8, u8))> {
    colors.min_by(|(_, a), (_, b)| {
        perceptual_distance(*a, target)
            .partial_cmp(&perceptual_distance(*b, target))
            .unwrap_or(std::cmp::Ordering::Equal)
    })
}

// Xterm colors; derived from https://ss64.com/bash/syntax-colors.html
pub const XTERM_COLORS: [(u8, u8, u8); 256] = [
    // The first 16 colors vary based on terminal theme, so these are likely not the actual colors
    // that are displayed when using these indices.
    (0, 0, 0),       //   0 Black (SYSTEM)
    (128, 0, 0),     //   1 Maroon (SYSTEM)
    (0, 128, 0),     //   2 Green (SYSTEM)
    (128, 128, 0),   //   3 Olive (SYSTEM)
    (0, 0, 128),     //   4 Navy (SYSTEM)
    (128, 0, 128),   //   5 Purple (SYSTEM)
    (0, 128, 128),   //   6 Teal (SYSTEM)
    (192, 192, 192), //   7 Silver (SYSTEM)
    (128, 128, 128), //   8 Grey (SYSTEM)
    (255, 0, 0),     //   9 Red (SYSTEM)
    (0, 255, 0),     //  10 Lime (SYSTEM)
    (255, 255, 0),   //  11 Yellow (SYSTEM)
    (0, 0, 255),     //  12 Blue (SYSTEM)
    (255, 0, 255),   //  13 Fuchsia (SYSTEM)
    (0, 255, 255),   //  14 Aqua (SYSTEM)
    (255, 255, 255), //  15 White (SYSTEM)
    // The rest of the colors are consistent in most terminals.
    (0, 0, 0),       //  16 Grey0
    (0, 0, 95),      //  17 NavyBlue
    (0, 0, 135),     //  18 DarkBlue
    (0, 0, 175),     //  19 Blue3
    (0, 0, 215),     //  20 Blue3
    (0, 0, 255),     //  21 Blue1
    (0, 95, 0),      //  22 DarkGreen
    (0, 95, 95),     //  23 DeepSkyBlue4
    (0, 95, 135),    //  24 DeepSkyBlue4
    (0, 95, 175),    //  25 DeepSkyBlue4
    (0, 95, 215),    //  26 DodgerBlue3
    (0, 95, 255),    //  27 DodgerBlue2
    (0, 135, 0),     //  28 Green4
    (0, 135, 95),    //  29 SpringGreen4
    (0, 135, 135),   //  30 Turquoise4
    (0, 135, 175),   //  31 DeepSkyBlue3
    (0, 135, 215),   //  32 DeepSkyBlue3
    (0, 135, 255),   //  33 DodgerBlue1
    (0, 175, 0),     //  34 Green3
    (0, 175, 95),    //  35 SpringGreen3
    (0, 175, 135),   //  36 DarkCyan
    (0, 175, 175),   //  37 LightSeaGreen
    (0, 175, 215),   //  38 DeepSkyBlue2
    (0, 175, 255),   //  39 DeepSkyBlue1
    (0, 215, 0),     //  40 Green3
    (0, 215, 95),    //  41 SpringGreen3
    (0, 215, 135),   //  42 SpringGreen2
    (0, 215, 175),   //  43 Cyan3
    (0, 215, 215),   //  44 DarkTurquoise
    (0, 215, 255),   //  45 Turquoise2
    (0, 255, 0),     //  46 Green1
    (0, 255, 95),    //  47 SpringGreen2
    (0, 255, 135),   //  48 SpringGreen1
    (0, 255, 175),   //  49 MediumSpringGreen
    (0, 255, 215),   //  50 Cyan2
    (0, 255, 255),   //  51 Cyan1
    (95, 0, 0),      //  52 DarkRed
    (95, 0, 95),     //  53 DeepPink4
    (95, 0, 135),    //  54 Purple4
    (95, 0, 175),    //  55 Purple4
    (95, 0, 215),    //  56 Purple3
    (95, 0, 255),    //  57 BlueViolet
    (95, 95, 0),     //  58 Orange4
    (95, 95, 95),    //  59 Grey37
    (95, 95, 135),   //  60 MediumPurple4
    (95, 95, 175),   //  61 SlateBlue3
    (95, 95, 215),   //  62 SlateBlue3
    (95, 95, 255),   //  63 RoyalBlue1
    (95, 135, 0),    //  64 Chartreuse4
    (95, 135, 95),   //  65 DarkSeaGreen4
    (95, 135, 135),  //  66 PaleTurquoise4
    (95, 135, 175),  //  67 SteelBlue
    (95, 135, 215),  //  68 SteelBlue3
    (95, 135, 255),  //  69 CornflowerBlue
    (95, 175, 0),    //  70 Chartreuse3
    (95, 175, 95),   //  71 DarkSeaGreen4
    (95, 175, 135),  //  72 CadetBlue
    (95, 175, 175),  //  73 CadetBlue
    (95, 175, 215),  //  74 SkyBlue3
    (95, 175, 255),  //  75 SteelBlue1
    (95, 215, 0),    //  76 Chartreuse3
    (95, 215, 95),   //  77 PaleGreen3
    (95, 215, 135),  //  78 SeaGreen3
    (95, 215, 175),  //  79 Aquamarine3
    (95, 215, 215),  //  80 MediumTurquoise
    (95, 215, 255),  //  81 SteelBlue1
    (95, 255, 0),    //  82 Chartreuse2
    (95, 255, 95),   //  83 SeaGreen2
    (95, 255, 135),  //  84 SeaGreen1
    (95, 255, 175),  //  85 SeaGreen1
    (95, 255, 215),  //  86 Aquamarine1
    (95, 255, 255),  //  87 DarkSlateGray2
    (135, 0, 0),     //  88 DarkRed
    (135, 0, 95),    //  89 DeepPink4
    (135, 0, 135),   //  90 DarkMagenta
    (135, 0, 175),   //  91 DarkMagenta
    (135, 0, 215),   //  92 DarkViolet
    (135, 0, 255),   //  93 Purple
    (135, 95, 0),    //  94 Orange4
    (135, 95, 95),   //  95 LightPink4
    (135, 95, 135),  //  96 Plum4
    (135, 95, 175),  //  97 MediumPurple3
    (135, 95, 215),  //  98 MediumPurple3
    (135, 95, 255),  //  99 SlateBlue1
    (135, 135, 0),   // 100 Yellow4
    (135, 135, 95),  // 101 Wheat4
    (135, 135, 135), // 102 Grey53
    (135, 135, 175), // 103 LightSlateGrey
    (135, 135, 215), // 104 MediumPurple
    (135, 135, 255), // 105 LightSlateBlue
    (135, 175, 0),   // 106 Yellow4
    (135, 175, 95),  // 107 DarkOliveGreen3
    (135, 175, 135), // 108 DarkSeaGreen
    (135, 175, 175), // 109 LightSkyBlue3
    (135, 175, 215), // 110 LightSkyBlue3
    (135, 175, 255), // 111 SkyBlue2
    (135, 215, 0),   // 112 Chartreuse2
    (135, 215, 95),  // 113 DarkOliveGreen3
    (135, 215, 135), // 114 PaleGreen3
    (135, 215, 175), // 115 DarkSeaGreen3
    (135, 215, 215), // 116 DarkSlateGray3
    (135, 215, 255), // 117 SkyBlue1
    (135, 255, 0),   // 118 Chartreuse1
    (135, 255, 95),  // 119 LightGreen
    (135, 255, 135), // 120 LightGreen
    (135, 255, 175), // 121 PaleGreen1
    (135, 255, 215), // 122 Aquamarine1
    (135, 255, 255), // 123 DarkSlateGray1
    (175, 0, 0),     // 124 Red3
    (175, 0, 95),    // 125 DeepPink4
    (175, 0, 135),   // 126 MediumVioletRed
    (175, 0, 175),   // 127 Magenta3
    (175, 0, 215),   // 128 DarkViolet
    (175, 0, 255),   // 129 Purple
    (175, 95, 0),    // 130 DarkOrange3
    (175, 95, 95),   // 131 IndianRed
    (175, 95, 135),  // 132 HotPink3
    (175, 95, 175),  // 133 MediumOrchid3
    (175, 95, 215),  // 134 MediumOrchid
    (175, 95, 255),  // 135 MediumPurple2
    (175, 135, 0),   // 136 DarkGoldenrod
    (175, 135, 95),  // 137 LightSalmon3
    (175, 135, 135), // 138 RosyBrown
    (175, 135, 175), // 139 Grey63
    (175, 135, 215), // 140 MediumPurple2
    (175, 135, 255), // 141 MediumPurple1
    (175, 175, 0),   // 142 Gold3
    (175, 175, 95),  // 143 DarkKhaki
    (175, 175, 135), // 144 NavajoWhite3
    (175, 175, 175), // 145 Grey69
    (175, 175, 215), // 146 LightSteelBlue3
    (175, 175, 255), // 147 LightSteelBlue
    (175, 215, 0),   // 148 Yellow3
    (175, 215, 95),  // 149 DarkOliveGreen3
    (175, 215, 135), // 150 DarkSeaGreen3
    (175, 215, 175), // 151 DarkSeaGreen2
    (175, 215, 215), // 152 LightCyan3
    (175, 215, 255), // 153 LightSkyBlue1
    (175, 255, 0),   // 154 GreenYellow
    (175, 255, 95),  // 155 DarkOliveGreen2
    (175, 255, 135), // 156 PaleGreen1
    (175, 255, 175), // 157 DarkSeaGreen2
    (175, 255, 215), // 158 DarkSeaGreen1
    (175, 255, 255), // 159 PaleTurquoise1
    (215, 0, 0),     // 160 Red3
    (215, 0, 95),    // 161 DeepPink3
    (215, 0, 135),   // 162 DeepPink3
    (215, 0, 175),   // 163 Magenta3
    (215, 0, 215),   // 164 Magenta3
    (215, 0, 255),   // 165 Magenta2
    (215, 95, 0),    // 166 DarkOrange3
    (215, 95, 95),   // 167 IndianRed
    (215, 95, 135),  // 168 HotPink3
    (215, 95, 175),  // 169 HotPink2
    (215, 95, 215),  // 170 Orchid
    (215, 95, 255),  // 171 MediumOrchid1
    (215, 135, 0),   // 172 Orange3
    (215, 135, 95),  // 173 LightSalmon3
    (215, 135, 135), // 174 LightPink3
    (215, 135, 175), // 175 Pink3
    (215, 135, 215), // 176 Plum3
    (215, 135, 255), // 177 Violet
    (215, 175, 0),   // 178 Gold3
    (215, 175, 95),  // 179 LightGoldenrod3
    (215, 175, 135), // 180 Tan
    (215, 175, 175), // 181 MistyRose3
    (215, 175, 215), // 182 Thistle3
    (215, 175, 255), // 183 Plum2
    (215, 215, 0),   // 184 Yellow3
    (215, 215, 95),  // 185 Khaki3
    (215, 215, 135), // 186 LightGoldenrod2
    (215, 215, 175), // 187 LightYellow3
    (215, 215, 215), // 188 Grey84
    (215, 215, 255), // 189 LightSteelBlue1
    (215, 255, 0),   // 190 Yellow2
    (215, 255, 95),  // 191 DarkOliveGreen1
    (215, 255, 135), // 192 DarkOliveGreen1
    (215, 255, 175), // 193 DarkSeaGreen1
    (215, 255, 215), // 194 Honeydew2
    (215, 255, 255), // 195 LightCyan1
    (255, 0, 0),     // 196 Red1
    (255, 0, 95),    // 197 DeepPink2
    (255, 0, 135),   // 198 DeepPink1
    (255, 0, 175),   // 199 DeepPink1
    (255, 0, 215),   // 200 Magenta2
    (255, 0, 255),   // 201 Magenta1
    (255, 95, 0),    // 202 OrangeRed1
    (255, 95, 95),   // 203 IndianRed1
    (255, 95, 135),  // 204 IndianRed1
    (255, 95, 175),  // 205 HotPink
    (255, 95, 215),  // 206 HotPink
    (255, 95, 255),  // 207 MediumOrchid1
    (255, 135, 0),   // 208 DarkOrange
    (255, 135, 95),  // 209 Salmon1
    (255, 135, 135), // 210 LightCoral
    (255, 135, 175), // 211 PaleVioletRed1
    (255, 135, 215), // 212 Orchid2
    (255, 135, 255), // 213 Orchid1
    (255, 175, 0),   // 214 Orange1
    (255, 175, 95),  // 215 SandyBrown
    (255, 175, 135), // 216 LightSalmon1
    (255, 175, 175), // 217 LightPink1
    (255, 175, 215), // 218 Pink1
    (255, 175, 255), // 219 Plum1
    (255, 215, 0),   // 220 Gold1
    (255, 215, 95),  // 221 LightGoldenrod2
    (255, 215, 135), // 222 LightGoldenrod2
    (255, 215, 175), // 223 NavajoWhite1
    (255, 215, 215), // 224 MistyRose1
    (255, 215, 255), // 225 Thistle1
    (255, 255, 0),   // 226 Yellow1
    (255, 255, 95),  // 227 LightGoldenrod1
    (255, 255, 135), // 228 Khaki1
    (255, 255, 175), // 229 Wheat1
    (255, 255, 215), // 230 Cornsilk1
    (255, 255, 255), // 231 Grey100
    (8, 8, 8),       // 232 Grey3
    (18, 18, 18),    // 233 Grey7
    (28, 28, 28),    // 234 Grey11
    (38, 38, 38),    // 235 Grey15
    (48, 48, 48),    // 236 Grey19
    (58, 58, 58),    // 237 Grey23
    (68, 68, 68),    // 238 Grey27
    (78, 78, 78),    // 239 Grey30
    (88, 88, 88),    // 240 Grey35
    (98, 98, 98),    // 241 Grey39
    (108, 108, 108), // 242 Grey42
    (118, 118, 118), // 243 Grey46
    (128, 128, 128), // 244 Grey50
    (138, 138, 138), // 245 Grey54
    (148, 148, 148), // 246 Grey58
    (158, 158, 158), // 247 Grey62
    (168, 168, 168), // 248 Grey66
    (178, 178, 178), // 249 Grey70
    (188, 188, 188), // 250 Grey74
    (198, 198, 198), // 251 Grey78
    (208, 208, 208), // 252 Grey82
    (218, 218, 218), // 253 Grey85
    (228, 228, 228), // 254 Grey89
    (238, 238, 238), // 255 Grey93
];

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

    #[test]
    fn nearest_xterm_fixed_color_distinct_from_skips_avoided_index() {
        let target = (29, 55, 66);
        let avoid = (48, 48, 48);

        let (target_idx, _) = nearest_xterm_fixed_color(target).expect("target color");
        let (avoid_idx, _) = nearest_xterm_fixed_color(avoid).expect("avoid color");
        assert_eq!(target_idx, 236);
        assert_eq!(avoid_idx, 236);

        let (distinct_idx, _) =
            nearest_xterm_fixed_color_distinct_from(target, avoid).expect("distinct color");
        assert_ne!(distinct_idx, avoid_idx);
    }

    #[test]
    fn terminal_palette_parse_osc_color_bel_terminated_short_rgb() {
        let response = b"\x1B]10;rgb:ff/00/7f\x07";

        assert_eq!(parse_osc_color_response(response, 10), Some((255, 0, 127)));
    }

    #[test]
    fn terminal_palette_parse_osc_color_st_terminated_long_rgb() {
        let response = b"\x1B]11;rgb:ffff/8000/0000\x1B\\";

        assert_eq!(parse_osc_color_response(response, 11), Some((255, 128, 0)));
    }

    #[test]
    fn terminal_palette_parse_osc_color_ignores_rgba_alpha() {
        let response = b"\x1B]10;rgba:aaaa/bbbb/cccc/dddd\x07";

        assert_eq!(
            parse_osc_color_response(response, 10),
            Some((170, 187, 204))
        );
    }

    #[test]
    fn terminal_palette_parse_osc_color_ignores_other_slot() {
        let response = b"\x1B]11;rgb:00/00/ff\x07";

        assert_eq!(parse_osc_color_response(response, 10), None);
    }

    #[test]
    fn terminal_palette_parse_osc_color_rejects_invalid_payload() {
        let response = b"\x1B]10;rgb:ff/00\x07";

        assert_eq!(parse_osc_color_response(response, 10), None);
    }
}
