// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>

use crate::cart_type::CartType;
use crate::error::Error;
use crate::licensee::Licensee;
use crate::licensee::NewLicensee;

use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::io::Write;
use std::str;

use byteorder::BigEndian;
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use byteorder::WriteBytesExt;

/// Represents the region the cartridge is made for.
#[derive(Debug, Copy, Clone)]
pub enum Region {
    Japan,
    Elsewhere,
}

impl fmt::Display for Region {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Japan => write!(fmt, "Japan")?,
            Self::Elsewhere => write!(fmt, "Not Japan")?,
        }
        Ok(())
    }
}

impl From<u8> for Region {
    fn from(data: u8) -> Self {
        match data {
            0 => Self::Japan,
            _ => Self::Elsewhere,
        }
    }
}

impl From<Region> for u8 {
    fn from(data: Region) -> Self {
        match data {
            Region::Japan => 0,
            Region::Elsewhere => 1,
        }
    }
}

/// Represents the metadata held by the GB/CGB internal header.
#[derive(Debug)]
pub struct Metadata {
    /// Application entry point. Bootrom jumps to this address once it is done.
    pub entry_point: u32,
    /// Bitmap of the Nintendo logo.
    pub nintendo_logo: [u8; 48],
    /// Title of the game cartridge. It is either less than 16, 15, or 11 ASCII bytes long,
    /// depending on if the [`Metadata::cgb_flag`] and/or [`Metadata::manufacturer_code`] are missing or
    /// found. On the actual cartridge, unused bytes are null characters.
    title: String,
    /// Manufacturer code, only available if the [`Header::cgb_flag`] is found.
    manufacturer_code: Option<[u8; 4]>,
    /// CGB flag, indicating support for CGB unique features or operation.
    cgb_flag: Option<u8>,
    /// New licensee code. This is used if the [`Header::licensee`] field is equal to 0x33.
    new_licensee: Option<NewLicensee>,
    /// Indicates support for Super Game Boy (SGB) functions.
    pub sgb_flag: bool,
    /// Metadataridge type. Specifically, what the configuration of the ROM, mapper, saves, etc. is.
    pub cart_type: CartType,
    /// The size of the ROM in KiB.
    pub rom_size: u16,
    /// The size of save RAM in KiB.
    pub save_ram_size: u8,
    /// The region the cartridge is made for.
    pub region: Region,
    /// The original licensee field. If it is 0x33, the licensee information is in [`Header::new_licensee`].
    licensee: Licensee,
    /// ROM version.
    pub rom_version: u8,
    /// The checksum of the GB/CGB header.
    pub header_checksum: u8,
    /// The checksum of the entire cartridge.
    pub checksum: u16,
}

impl Metadata {
    /// Prints the stored logo to stdout.
    ///
    /// Prints two spaces or two '█' per pixel due to terminal characters being taller than they
    /// are wider.
    pub fn print_logo(&self) {
        let output = self.extract_boot_logo();
        for y in 0..16 {
            for x in 0..48 {
                if output[x + y * 48] == 0 {
                    print!("  ");
                } else {
                    print!("\u{2588}\u{2588}");
                }
            }
            println!();
        }
    }

    /// Extracts the boot logo and returns it as a 48x16 8bit grayscale image.
    fn extract_boot_logo(&self) -> [u8; 48 * 16] {
        // 2 pairs of 2 bytes x 4 form a single tile
        // First byte has low bits, second high bits

        let logo = self.nintendo_logo;
        let mut output = [0u8; 48 * 16];
        // Each 4 bytes comprise of a tile of 8x4 (that's stretched to 8x8) pixels
        for i in (0..logo.len()).step_by(4) {
            let tile_num = i / 4;
            let output_index = ((tile_num % 6) * 8) + ((tile_num / 6) * 48 * 8);
            // Tiles are arranged as:
            // | byte 0 | byte 2 |
            // | byte 1 | byte 3 |
            //
            // And each byte has two rows of pixels, one bit per pixel:
            // MSB | 6 | 5 |  4
            //  3  | 2 | 1 | LSB
            //
            // The boot logo tiles are half the display height, when rendered each row needs to be
            // doubled.

            // Process 4 pixels at a time, which are extracted from just a single byte of data.
            for y in (0..7).step_by(4) {
                for x in 0..8 {
                    let byte_index = (x / 4) * 2 + (y / 4);
                    let byte = logo[i + byte_index];
                    // Renders one byte from a tile
                    let output_index = output_index + x + y * 48;
                    let bit = x % 4;
                    output[output_index] = ((byte >> (7 - bit)) & 0x1) * 0xFF;
                    output[output_index + 48] = ((byte >> (7 - bit)) & 0x1) * 0xFF;
                    output[output_index + 48 * 2] = ((byte >> (3 - bit)) & 0x1) * 0xFF;
                    output[output_index + 48 * 3] = ((byte >> (3 - bit)) & 0x1) * 0xFF;
                }
            }
        }
        output
    }
}

pub trait MetadataRead {
    /// Parses the GB ROM metadata from the object provided, returning a [`Metadata`] object with the
    /// header metadata.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
    /// an unexpected value.
    /// Returns [`Error::Io`] if an IO error took place while reading.
    fn read_gb_cart(&mut self) -> Result<Metadata, Error>;
}

pub trait MetadataWrite {
    /// Takes the given cart metadata and writes it using the current object.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
    /// an unexpected value.
    /// Returns [`Error::Io`] if an IO error took place while writing.
    fn write_gb_cart(&mut self, header: &Metadata) -> Result<(), Error>;
}

/// Helper function to identify valid characters in GB header fields
///
/// Valid characters should only be A-Z0-9, whitespace, and other visual characters. Or in other
/// words, apparently everything printable except lowercase characters.
fn valid_characters_header(slice: &[u8]) -> bool {
    slice
        .iter()
        .all(|b| !b.is_ascii_lowercase() && (b.is_ascii_graphic() || b.is_ascii_whitespace()))
}

impl<T: Read + Seek> MetadataRead for T {
    fn read_gb_cart(&mut self) -> Result<Metadata, Error> {
        self.seek(SeekFrom::Start(0x100))?;

        let entry_point = self.read_u32::<LittleEndian>()?;
        let mut nintendo_logo = [0u8; 48];
        self.read_exact(&mut nintendo_logo)?;
        let mut title = [0u8; 16];
        self.read_exact(&mut title)?;

        let cgb_flag = if title[15].is_ascii() {
            None
        } else {
            Some(title[15])
        };

        // This is a bit convoluted, effectively try to find the span of bytes representing the
        // title, which should be either null terminated, or go all the way to the end. If it goes
        // all the way to the end, there's no manufacturer code to extract. If all manufacturer
        // bytes are \0, then there's also no manufacturer code.
        let null_position = title.iter().position(|&b| b == 0x00);
        let manufacturer_code = if let Some(position) = null_position
            && position < 12
            && title[11..15].iter().all(|b| *b != 0u8)
        {
            let mut code = [0u8; 4];
            code.copy_from_slice(&title[11..15]);
            if !valid_characters_header(&code) {
                return Err(Error::Parse(
                    "manufacturer code is not all uppercase ASCII".into(),
                ));
            }
            Some(code)
        } else {
            None
        };

        let title = null_position.map_or_else(|| &title[..], |position| &title[..position]);
        if !valid_characters_header(title) {
            return Err(Error::Parse("title is not all uppercase ASCII".into()));
        }
        let title = str::from_utf8(title)?.trim_matches('\0').to_string();

        let mut new_licensee = [0u8; 2];
        self.read_exact(&mut new_licensee)?;
        let new_licensee = NewLicensee::from(new_licensee);
        let sgb_flag = self.read_u8()? == 0x03;
        let cart_type = self.read_u8()?.into();
        let rom_size = self.read_u8()?;
        if rom_size > 8 {
            return Err(Error::Parse("ROM size is too large".into()));
        }
        let rom_size: u16 = 32 * (1 << rom_size);
        let save_ram_size = self.read_u8()?;
        let save_ram_size = match save_ram_size {
            0 => 0,
            2 => 8,
            3 => 32,
            4 => 128,
            5 => 64,
            _ => return Err(Error::Parse("unknown RAM size".into())),
        };
        let region = self.read_u8()?.into();
        let licensee = self.read_u8()?.into();
        let rom_version = self.read_u8()?;
        let header_checksum = self.read_u8()?;
        let checksum = self.read_u16::<BigEndian>()?;

        let new_licensee = match licensee {
            Licensee::NewLicenseeField => Some(new_licensee),
            _ => None,
        };

        Ok(Metadata {
            entry_point,
            nintendo_logo,
            title,
            manufacturer_code,
            cgb_flag,
            new_licensee,
            sgb_flag,
            cart_type,
            rom_size,
            save_ram_size,
            region,
            licensee,
            rom_version,
            header_checksum,
            checksum,
        })
    }
}

impl Metadata {
    /// Returns the checksum of the header.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Io`] if there are any IO errors while reading or seeking from the cart's
    /// underlying IO object.
    pub fn header_checksum<T: Seek + Read>(&self, io: &mut T) -> Result<u8, Error> {
        io.seek(SeekFrom::Start(0x134))?;
        let mut checksum = 0u8;
        for _ in 0..(0x14D - 0x134) {
            checksum = checksum.wrapping_sub(io.read_u8()?).wrapping_sub(1);
        }
        Ok(checksum)
    }

    /// Returns the checksum of the entire cart.
    ///
    /// The checksum is simply the sum of every byte in the cart, truncated to 16 bits.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Io`] if there are any IO errors while reading or seeking from the cart's
    /// underlying IO object.
    pub fn checksum<T: Seek + Read>(&self, io: &mut T) -> Result<u16, Error> {
        io.seek(SeekFrom::Start(0))?;
        let mut checksum = 0u16;
        let mut data = Vec::new();
        // It's faster to read in the entire ROM... on modern systems, the max size fits in memory
        // just fine, so just read it all
        io.read_to_end(&mut data)?;
        // Null out current checksum values, they're not part of checksum calculation
        data[0x14E] = 0;
        data[0x14F] = 0;
        for byte in &data {
            checksum = checksum.wrapping_add(u16::from(*byte));
        }
        Ok(checksum)
    }

    /// Sets the title in the title field of the header.
    ///
    /// The max length of the title field varies depending on whether the header has a CGB flag
    /// and/or if it has a manufacturer code. If it doesn't have either, the total length is 16
    /// ASCII characters, if it has a CGB field it's one less, and if it has a manufacturer code
    /// it's four less. In other words, all possible sizes for this field are 16, 15, or 11. It
    /// looks like the most common sizes are 16 and 11, though.
    ///
    /// # Errors
    /// Returns an [`Error::Parse`] if the given title won't fit based on the maximum acceptable
    /// length depending on the existence of a CGB field and/or manufacturer code.
    pub fn set_title(&mut self, title: &str) -> Result<(), Error> {
        let title_max = if self.manufacturer_code.is_some() {
            11
        } else if self.cgb_flag.is_some() {
            15
        } else {
            16
        };
        if title.len() > title_max {
            return Err(Error::Parse(format!(
                "title is too long, max size is {title_max}"
            )));
        }
        self.title = title.into();
        Ok(())
    }

    /// Gets the current title field.
    #[must_use]
    pub fn title(&self) -> &str {
        &self.title
    }

    /// Sets the manufacturer code.
    ///
    /// # Errors
    /// There cannot be a manufacturer code if the title is longer than 11 ASCII characters, so
    /// trying to set one in that case will yield [`Error::Parse`].
    pub fn set_manufacturer_code(&mut self, code: [u8; 4]) -> Result<(), Error> {
        if self.title.len() > 11 {
            return Err(Error::Parse(
                "title is too long to set manufacturer code".into(),
            ));
        }
        self.manufacturer_code = Some(code);
        Ok(())
    }

    /// Returns the manufacturer code, if there is one.
    #[must_use]
    pub const fn manufacturer_code(&self) -> Option<[u8; 4]> {
        self.manufacturer_code
    }

    /// Sets the CGB flag.
    ///
    /// # Errors
    /// There cannot be a CGB flag if the title is 16 ASCII characters long, so trying to set one
    /// in that case will yield [`Error::Parse`].
    pub fn set_cgb_flag(&mut self, flag: u8) -> Result<(), Error> {
        if self.title.len() == 16 {
            return Err(Error::Parse("title is too long to set CGB flag".into()));
        }
        self.cgb_flag = Some(flag);
        Ok(())
    }

    /// Returns the CGB flag, if there is one.
    #[must_use]
    pub const fn cgb_flag(&self) -> Option<u8> {
        self.cgb_flag
    }

    /// Sets the new licensee field.
    ///
    /// This automatically updates the "old" licensee field to indicate that there is a new
    /// licensee field that should be used.
    ///
    /// To clear this field, use `[Self::set_licensee]` and set the "old" licensee field to
    /// anything other than [`Licensee::NewLicenseeField`].
    pub const fn set_new_licensee(&mut self, new_licensee: NewLicensee) {
        self.licensee = Licensee::NewLicenseeField;
        self.new_licensee = Some(new_licensee);
    }

    /// Returns the current new licensee field, if there is one.
    #[must_use]
    pub const fn new_licensee(&self) -> Option<NewLicensee> {
        self.new_licensee
    }

    /// Sets the "old" licensee field.
    ///
    /// On success, it clears the new licensee field. To set the new licensee field, use
    /// [`Self::set_new_licensee`] instead.
    ///
    /// # Errors
    /// Returns [`Error::Parse`] if the licensee field requested is [`Licensee::NewLicenseeField`].
    /// To set the new licensee field, use [`Self::set_new_licensee`] instead.
    pub fn set_licensee(&mut self, licensee: Licensee) -> Result<(), Error> {
        if matches!(licensee, Licensee::NewLicenseeField) {
            Err(Error::Parse(
                "cannot set the new licensee field this way".into(),
            ))
        } else {
            self.licensee = licensee;
            self.new_licensee = None;
            Ok(())
        }
    }

    /// Returns the current "old" licensee field.
    #[must_use]
    pub const fn licensee(&self) -> Licensee {
        self.licensee
    }

    /// Builds a [`Metadata`] from an IO object.
    ///
    /// # Errors
    /// See [`MetadataRead::read_gb_cart`].
    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.read_gb_cart()
    }
}

impl<T: Write + Seek> MetadataWrite for T {
    fn write_gb_cart(&mut self, header: &Metadata) -> Result<(), Error> {
        self.seek(SeekFrom::Start(0x100))?;

        self.write_u32::<LittleEndian>(header.entry_point)?;
        self.write_all(&header.nintendo_logo)?;
        let mut title = [0u8; 16];
        title[..header.title.len()].copy_from_slice(header.title.as_bytes());
        self.write_all(&title)?;
        if let Some(manufacturer_code) = header.manufacturer_code {
            self.seek(SeekFrom::Start(0x013F))?;
            self.write_all(&manufacturer_code)?;
        }
        if let Some(cgb_flag) = header.cgb_flag {
            self.seek(SeekFrom::Start(0x143))?;
            self.write_u8(cgb_flag)?;
        }
        self.write_u16::<LittleEndian>(
            header
                .new_licensee
                .unwrap_or(NewLicensee::Unknown(0))
                .into(),
        )?;
        self.write_u8(if header.sgb_flag { 0x03 } else { 0x00 })?;
        self.write_u8(header.cart_type.into())?;
        let rom_size = u8::try_from((header.rom_size / 32).trailing_zeros());
        if rom_size.is_err() {
            return Err(Error::Parse("invalid ROM size".into()));
        }
        self.write_u8(rom_size.unwrap())?;

        let save_ram_size = match header.save_ram_size {
            0 => 0x0u8,
            8 => 0x2,
            32 => 0x3,
            128 => 0x4,
            64 => 0x5,
            _ => return Err(Error::Parse("Unknown RAM size".into())),
        };
        self.write_u8(save_ram_size)?;
        self.write_u8(header.region.into())?;
        self.write_u8(header.licensee.into())?;
        self.write_u8(header.rom_version)?;
        self.write_u8(header.header_checksum)?;
        self.write_u16::<BigEndian>(header.checksum)?;

        Ok(())
    }
}

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

    const OLD_HEADER: [u8; 0x50] = [
        // Entry point (0x0100 - 0x0103)
        0x00, 0xc3, 0x6e, 0x01, //
        // Logo bytes (I'm writing garbage here :) (0x0104 - 0x0133)
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        // Title (0x0134 - 0x0143*) *last 5 bytes can be something else...
        // "UNIT TEST"
        0x55, 0x4E, 0x49, 0x54, 0x20, 0x54, 0x45, 0x53, 0x54, 0x00, 0x00, //
        0x00, 0x00, 0x00, 0x00, // Manufacturer code, ignored on old
        0x00, // CGB flag, ignored on old
        // New licensee code (0x0144 - 0x0145)
        0x00, 0x00, //
        // SGB flag (0x0146)
        0x00, //
        // Metadataridge type (0x0147)
        0x10, // MBC3+TIMER+RAM+BATTERY
        // ROM size (0x0148)
        0x08, // 8MiB, 512 banks
        // RAM size (0x0149)
        0x05, // 64KiB, 8 banks of 8KiB
        // Destination code (0x014A)
        0x01, // Not Japan
        // Old licensee code (0x014B)
        0xE9, // Natsume
        // Mask ROM version number (0x014C)
        0xFF, //
        // Header checksum (0x014D)
        0x41, //
        // Global checksum (0x014E - 0x014F)
        0x30, 0xD9, //
    ];

    #[test]
    fn old_cart() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart().unwrap();

        assert_eq!(cart.entry_point, 0x016EC300);
        let fake_logo: [u8; 16 * 3] = [
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        ];
        assert_eq!(cart.nintendo_logo, fake_logo);
        assert_eq!(cart.title, "UNIT TEST");
        assert_eq!(cart.manufacturer_code, None);
        assert_eq!(cart.cgb_flag, None);
        assert!(matches!(cart.new_licensee, None));
        assert_eq!(cart.sgb_flag, false);
        assert!(matches!(cart.cart_type, CartType::Mbc3TimerRamBattery));
        assert_eq!(cart.rom_size, 8 * 1024);
        assert_eq!(cart.save_ram_size, 64);
        assert!(matches!(cart.region, Region::Elsewhere));
        assert!(matches!(cart.licensee, Licensee::Natsume));
        assert_eq!(cart.rom_version, 255);
        assert_eq!(
            cart.header_checksum(&mut fake_cart).unwrap(),
            cart.header_checksum
        );
        assert_eq!(cart.checksum(&mut fake_cart).unwrap(), cart.checksum);
    }

    const NEW_HEADER: [u8; 0x50] = [
        // Entry point (0x0100 - 0x0103)
        0x00, 0xc3, 0x6e, 0x01, //
        // Logo bytes (I'm writing garbage here :) (0x0104 - 0x0133)
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        // Title (0x0134 - 0x0143*) *last 5 bytes can be something else...
        // "UNIT TEST"
        0x55, 0x4E, 0x49, 0x54, 0x20, 0x54, 0x45, 0x53, 0x54, 0x00, 0x00, //
        0x54, 0x45, 0x53, 0x54, // Manufacturer code, TEST
        0x80, // CGB flag
        // New licensee code (0x0144 - 0x0145)
        0x36, 0x34, // LucasArts
        // SGB flag (0x0146)
        0x00, //
        // Metadataridge type (0x0147)
        0x10, // MBC3+TIMER+RAM+BATTERY
        // ROM size (0x0148)
        0x08, // 8MiB, 512 banks
        // RAM size (0x0149)
        0x05, // 64KiB, 8 banks of 8KiB
        // Destination code (0x014A)
        0x01, // Not Japan
        // Old licensee code (0x014B)
        0x33, // use New Licensee
        // Mask ROM version number (0x014C)
        0xFF, //
        // Header checksum (0x014D)
        0xCD, //
        // Global checksum (0x014E - 0x014F)
        0x32, 0xD9, //
    ];

    #[test]
    fn new_cart() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart().unwrap();

        assert_eq!(cart.entry_point, 0x016EC300);
        let fake_logo: [u8; 16 * 3] = [
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
            0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, 0xCA, 0xFE, //
        ];
        assert_eq!(cart.nintendo_logo, fake_logo);
        assert_eq!(cart.title, "UNIT TEST");
        assert_eq!(cart.manufacturer_code, Some([0x54, 0x45, 0x53, 0x54]));
        assert_eq!(cart.cgb_flag, Some(0x80));
        assert!(matches!(cart.new_licensee, Some(NewLicensee::Lucasarts)));
        assert_eq!(cart.sgb_flag, false);
        assert!(matches!(cart.cart_type, CartType::Mbc3TimerRamBattery));
        assert_eq!(cart.rom_size, 8 * 1024);
        assert_eq!(cart.save_ram_size, 64);
        assert!(matches!(cart.region, Region::Elsewhere));
        assert!(matches!(cart.licensee, Licensee::NewLicenseeField));
        assert_eq!(cart.rom_version, 255);
        assert_eq!(
            cart.header_checksum(&mut fake_cart).unwrap(),
            cart.header_checksum
        );
        assert_eq!(cart.checksum(&mut fake_cart).unwrap(), cart.checksum);
    }

    #[test]
    fn new_cart_bad_title() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart[0x135] = 0x80; // Make title non-ASCII
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "title is not all uppercase ASCII"))
        );
        fake_cart.get_mut()[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart.get_mut()[0x13F] = 0x80; // Make manufacturer code not ASCII

        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
        );

        fake_cart.get_mut()[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart.get_mut()[0x13F] = 0x61; // Make manufacturer code lowercase ascii

        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
        );
    }

    #[test]
    fn new_cart_test_set_title() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        let mut fake_cart = Cursor::new(fake_cart);
        let mut cart = fake_cart.read_gb_cart().unwrap();

        let result = cart.set_title("12345678901234567890"); // 20 chars
        assert!(
            matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 11"))
        );

        let result = cart.set_title("123456789012"); // 12 chars
        assert!(
            matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 11"))
        );

        let result = cart.set_title("12345678901"); // 11 chars
        assert!(result.is_ok());
        assert_eq!(cart.title(), "12345678901");
    }

    #[test]
    fn old_cart_test_set_title() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
        let mut fake_cart = Cursor::new(fake_cart);
        let mut cart = fake_cart.read_gb_cart().unwrap();

        let result = cart.set_title("12345678901234567890"); // 20 chars
        assert!(
            matches!(result, Err(err) if matches!(&err, Error::Parse(string) if string == "title is too long, max size is 16"))
        );

        let result = cart.set_title("1234567890123456"); // 16 chars
        assert!(result.is_ok());
        assert_eq!(cart.title(), "1234567890123456");
    }

    #[test]
    fn new_cart_bad_manufacturer_code() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart[0x140] = 0x80; // Make manufacturer code non-ASCII
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "manufacturer code is not all uppercase ASCII"))
        );
    }

    #[test]
    fn new_cart_bad_rom_size() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart[0x148] = 0x9; // Make ROM size > 8, which should be invalid
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "ROM size is too large"))
        );
    }

    #[test]
    fn new_cart_bad_save_ram_size() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        fake_cart[0x149] = 0x6; // Make RAM size invalid
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart();
        assert!(
            matches!(cart, Err(err) if matches!(&err, Error::Parse(string) if string == "unknown RAM size"))
        );
    }

    #[test]
    fn new_cart_read_write() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&NEW_HEADER);
        let original = fake_cart.clone();
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart().unwrap();

        let fake_cart = vec![0u8; 8 * 1024 * 1024];
        let mut fake_cart = Cursor::new(fake_cart);
        fake_cart.write_gb_cart(&cart).unwrap();
        let fake_cart = fake_cart.into_inner();
        assert_eq!(fake_cart[0x100..0x150], original[0x100..0x150]);
        assert_eq!(fake_cart, original);
    }

    #[test]
    fn old_cart_read_write() {
        let mut fake_cart = vec![0u8; 8 * 1024 * 1024]; // 8MiB
        fake_cart[0x100..0x150].copy_from_slice(&OLD_HEADER);
        let original = fake_cart.clone();
        let mut fake_cart = Cursor::new(fake_cart);
        let cart = fake_cart.read_gb_cart().unwrap();

        let fake_cart = vec![0u8; 8 * 1024 * 1024];
        let mut fake_cart = Cursor::new(fake_cart);
        fake_cart.write_gb_cart(&cart).unwrap();
        let fake_cart = fake_cart.into_inner();
        assert_eq!(fake_cart[0x100..0x150], original[0x100..0x150]);
        assert_eq!(fake_cart, original);
    }
}
