pub(crate) mod device;
#[cfg(feature = "emulation-rand")]
mod rand;

use device::{chrome::*, firefox::*, okhttp::*, opera::*, safari::*};
#[cfg(feature = "emulation-serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "emulation-rand")]
use strum_macros::VariantArray;
use typed_builder::TypedBuilder;

macro_rules! define_enum {
    (
        $(#[$meta:meta])*
        with_dispatch,
        $name:ident, $default_variant:ident,
        $(
            $variant:ident => ($rename:expr, $emulation_fn:path)
        ),* $(,)?
    ) => {
        $(#[$meta])*
        #[non_exhaustive]
        #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
        #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
        #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
        pub enum $name {
            $(
                #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
                $variant,
            )*
        }

        impl Default for $name {
            fn default() -> Self {
                $name::$default_variant
            }
        }

        impl $name {
            pub fn into_emulation(self, opt: EmulationOption) -> hpx::Emulation {
                match self {
                    $(
                        $name::$variant => $emulation_fn(opt),
                    )*
                }
            }
        }
    };

    (
        $(#[$meta:meta])*
        plain,
        $name:ident, $default_variant:ident,
        $(
            $variant:ident => $rename:expr
        ),* $(,)?
    ) => {
        $(#[$meta])*
        #[non_exhaustive]
        #[derive(Clone, Copy, Hash, Debug, PartialEq, Eq)]
        #[cfg_attr(feature = "emulation-rand", derive(VariantArray))]
        #[cfg_attr(feature = "emulation-serde", derive(Deserialize, Serialize))]
        pub enum $name {
            $(
                #[cfg_attr(feature = "emulation-serde", serde(rename = $rename))]
                $variant,
            )*
        }

        impl Default for $name {
            fn default() -> Self {
                $name::$default_variant
            }
        }
    };
}

define_enum!(
    /// Represents different browser versions for emulation.
    ///
    /// The `Emulation` enum provides variants for different browser versions that can be used
    /// to emulate HTTP requests. Each variant corresponds to a specific browser version.
    ///
    /// # Naming Convention
    ///
    /// The naming convention for the variants follows the pattern `browser_version`, where
    /// `browser` is the name of the browser (e.g., `chrome`, `firefox`, `safari`) and `version`
    /// is the version number. For example, `Chrome100` represents Chrome version 100.
    ///
    /// The serialized names of the variants use underscores to separate the browser name and
    /// version number, following the pattern `browser_version`. For example, `Chrome100` is
    /// serialized as `"chrome_100"`.
    with_dispatch,
    Emulation, Chrome143,

    // Chrome versions
    Chrome100 => ("chrome_100", v100::emulation),
    Chrome101 => ("chrome_101", v101::emulation),
    Chrome104 => ("chrome_104", v104::emulation),
    Chrome105 => ("chrome_105", v105::emulation),
    Chrome106 => ("chrome_106", v106::emulation),
    Chrome107 => ("chrome_107", v107::emulation),
    Chrome108 => ("chrome_108", v108::emulation),
    Chrome109 => ("chrome_109", v109::emulation),
    Chrome110 => ("chrome_110", v110::emulation),
    Chrome114 => ("chrome_114", v114::emulation),
    Chrome116 => ("chrome_116", v116::emulation),
    Chrome117 => ("chrome_117", v117::emulation),
    Chrome118 => ("chrome_118", v118::emulation),
    Chrome119 => ("chrome_119", v119::emulation),
    Chrome120 => ("chrome_120", v120::emulation),
    Chrome123 => ("chrome_123", v123::emulation),
    Chrome124 => ("chrome_124", v124::emulation),
    Chrome126 => ("chrome_126", v126::emulation),
    Chrome127 => ("chrome_127", v127::emulation),
    Chrome128 => ("chrome_128", v128::emulation),
    Chrome129 => ("chrome_129", v129::emulation),
    Chrome130 => ("chrome_130", v130::emulation),
    Chrome131 => ("chrome_131", v131::emulation),
    Chrome132 => ("chrome_132", v132::emulation),
    Chrome133 => ("chrome_133", v133::emulation),
    Chrome134 => ("chrome_134", v134::emulation),
    Chrome135 => ("chrome_135", v135::emulation),
    Chrome136 => ("chrome_136", v136::emulation),
    Chrome137 => ("chrome_137", v137::emulation),
    Chrome138 => ("chrome_138", v138::emulation),
    Chrome139 => ("chrome_139", v139::emulation),
    Chrome140 => ("chrome_140", v140::emulation),
    Chrome141 => ("chrome_141", v141::emulation),
    Chrome142 => ("chrome_142", v142::emulation),
    Chrome143 => ("chrome_143", v143::emulation),
    Chrome144 => ("chrome_144", v144::emulation),
    Chrome145 => ("chrome_145", v145::emulation),
    Chrome146 => ("chrome_146", v146::emulation),
    Chrome147 => ("chrome_147", v147::emulation),
    Chrome148 => ("chrome_148", v148::emulation),
    Chrome149 => ("chrome_149", v149::emulation),

    // Edge versions
    Edge101 => ("edge_101", edge101::emulation),
    Edge122 => ("edge_122", edge122::emulation),
    Edge127 => ("edge_127", edge127::emulation),
    Edge131 => ("edge_131", edge131::emulation),
    Edge134 => ("edge_134", edge134::emulation),
    Edge135 => ("edge_135", edge135::emulation),
    Edge136 => ("edge_136", edge136::emulation),
    Edge137 => ("edge_137", edge137::emulation),
    Edge138 => ("edge_138", edge138::emulation),
    Edge139 => ("edge_139", edge139::emulation),
    Edge140 => ("edge_140", edge140::emulation),
    Edge141 => ("edge_141", edge141::emulation),
    Edge142 => ("edge_142", edge142::emulation),
    Edge143 => ("edge_143", edge143::emulation),
    Edge144 => ("edge_144", edge144::emulation),
    Edge145 => ("edge_145", edge145::emulation),
    Edge146 => ("edge_146", edge146::emulation),
    Edge147 => ("edge_147", edge147::emulation),
    Edge148 => ("edge_148", edge148::emulation),

    // Opera versions
    Opera116 => ("opera_116", opera116::emulation),
    Opera117 => ("opera_117", opera117::emulation),
    Opera118 => ("opera_118", opera118::emulation),
    Opera119 => ("opera_119", opera119::emulation),
    Opera120 => ("opera_120", opera120::emulation),
    Opera121 => ("opera_121", opera121::emulation),
    Opera122 => ("opera_122", opera122::emulation),
    Opera123 => ("opera_123", opera123::emulation),
    Opera124 => ("opera_124", opera124::emulation),
    Opera125 => ("opera_125", opera125::emulation),
    Opera126 => ("opera_126", opera126::emulation),
    Opera127 => ("opera_127", opera127::emulation),
    Opera128 => ("opera_128", opera128::emulation),
    Opera129 => ("opera_129", opera129::emulation),
    Opera130 => ("opera_130", opera130::emulation),
    Opera131 => ("opera_131", opera131::emulation),

    // Safari versions
    SafariIos17_2 => ("safari_ios_17.2", safari_ios_17_2::emulation),
    SafariIos17_4_1 => ("safari_ios_17.4.1", safari_ios_17_4_1::emulation),
    SafariIos16_5 => ("safari_ios_16.5", safari_ios_16_5::emulation),
    Safari15_3 => ("safari_15.3", safari15_3::emulation),
    Safari15_5 => ("safari_15.5", safari15_5::emulation),
    Safari15_6_1 => ("safari_15.6.1", safari15_6_1::emulation),
    Safari16 => ("safari_16", safari16::emulation),
    Safari16_5 => ("safari_16.5", safari16_5::emulation),
    Safari17_0 => ("safari_17.0", safari17_0::emulation),
    Safari17_2_1 => ("safari_17.2.1", safari17_2_1::emulation),
    Safari17_4_1 => ("safari_17.4.1", safari17_4_1::emulation),
    Safari17_5 => ("safari_17.5", safari17_5::emulation),
    Safari17_6 => ("safari_17.6", safari17_6::emulation),
    Safari18 => ("safari_18", safari18::emulation),
    SafariIPad18 => ("safari_ipad_18", safari_ipad_18::emulation),
    Safari18_2 => ("safari_18.2", safari18_2::emulation),
    SafariIos18_1_1 => ("safari_ios_18.1.1", safari_ios_18_1_1::emulation),
    Safari18_3 => ("safari_18.3", safari18_3::emulation),
    Safari18_3_1 => ("safari_18.3.1", safari18_3_1::emulation),
    Safari18_5 => ("safari_18.5", safari18_5::emulation),
    Safari26 => ("safari_26", safari26::emulation),
    Safari26_1 => ("safari_26.1", safari26_1::emulation),
    Safari26_2 => ("safari_26.2", safari26_2::emulation),
    SafariIPad26 => ("safari_ipad_26", safari_ipad_26::emulation),
    SafariIPad26_2 => ("safari_ipad_26.2", safari_ipad_26_2::emulation),
    SafariIos26 => ("safari_ios_26", safari_ios_26::emulation),
    SafariIos26_2 => ("safari_ios_26.2", safari_ios_26_2::emulation),
    Safari19 => ("safari_19", safari19::emulation),
    SafariIos19 => ("safari_ios_19", safari_ios_19::emulation),
    SafariIPad19 => ("safari_ipad_19", safari_ipad_19::emulation),
    Safari20 => ("safari_20", safari20::emulation),
    SafariIos20 => ("safari_ios_20", safari_ios_20::emulation),
    SafariIPad20 => ("safari_ipad_20", safari_ipad_20::emulation),
    Safari21 => ("safari_21", safari21::emulation),
    SafariIos21 => ("safari_ios_21", safari_ios_21::emulation),
    SafariIPad21 => ("safari_ipad_21", safari_ipad_21::emulation),
    Safari22 => ("safari_22", safari22::emulation),
    SafariIos22 => ("safari_ios_22", safari_ios_22::emulation),
    SafariIPad22 => ("safari_ipad_22", safari_ipad_22::emulation),
    Safari23 => ("safari_23", safari23::emulation),
    SafariIos23 => ("safari_ios_23", safari_ios_23::emulation),
    SafariIPad23 => ("safari_ipad_23", safari_ipad_23::emulation),
    Safari24 => ("safari_24", safari24::emulation),
    SafariIos24 => ("safari_ios_24", safari_ios_24::emulation),
    SafariIPad24 => ("safari_ipad_24", safari_ipad_24::emulation),
    Safari25 => ("safari_25", safari25::emulation),
    SafariIos25 => ("safari_ios_25", safari_ios_25::emulation),
    SafariIPad25 => ("safari_ipad_25", safari_ipad_25::emulation),
    Safari26_3 => ("safari_26.3", safari26_3::emulation),
    SafariIos26_3 => ("safari_ios_26.3", safari_ios_26_3::emulation),
    SafariIPad26_3 => ("safari_ipad_26.3", safari_ipad_26_3::emulation),
    Safari26_4 => ("safari_26.4", safari26_4::emulation),
    SafariIos26_4 => ("safari_ios_26.4", safari_ios_26_4::emulation),
    SafariIPad26_4 => ("safari_ipad_26.4", safari_ipad_26_4::emulation),

    // Firefox versions
    Firefox109 => ("firefox_109", ff109::emulation),
    Firefox117 => ("firefox_117", ff117::emulation),
    Firefox128 => ("firefox_128", ff128::emulation),
    Firefox133 => ("firefox_133", ff133::emulation),
    Firefox135 => ("firefox_135", ff135::emulation),
    FirefoxPrivate135 => ("firefox_private_135", ff_private_135::emulation),
    FirefoxAndroid135 => ("firefox_android_135", ff_android_135::emulation),
    Firefox136 => ("firefox_136", ff136::emulation),
    FirefoxPrivate136 => ("firefox_private_136", ff_private_136::emulation),
    Firefox137 => ("firefox_137", ff137::emulation),
    Firefox138 => ("firefox_138", ff138::emulation),
    Firefox139 => ("firefox_139", ff139::emulation),
    Firefox140 => ("firefox_140", ff140::emulation),
    Firefox141 => ("firefox_141", ff141::emulation),
    Firefox142 => ("firefox_142", ff142::emulation),
    Firefox143 => ("firefox_143", ff143::emulation),
    Firefox144 => ("firefox_144", ff144::emulation),
    Firefox145 => ("firefox_145", ff145::emulation),
    Firefox146 => ("firefox_146", ff146::emulation),
    Firefox147 => ("firefox_147", ff147::emulation),
    Firefox148 => ("firefox_148", ff148::emulation),
    Firefox149 => ("firefox_149", ff149::emulation),
    Firefox150 => ("firefox_150", ff150::emulation),
    Firefox151 => ("firefox_151", ff151::emulation),

    // OkHttp versions
    OkHttp3_9 => ("okhttp_3.9", okhttp3_9::emulation),
    OkHttp3_11 => ("okhttp_3.11", okhttp3_11::emulation),
    OkHttp3_13 => ("okhttp_3.13", okhttp3_13::emulation),
    OkHttp3_14 => ("okhttp_3.14", okhttp3_14::emulation),
    OkHttp4_9 => ("okhttp_4.9", okhttp4_9::emulation),
    OkHttp4_10 => ("okhttp_4.10", okhttp4_10::emulation),
    OkHttp4_12 => ("okhttp_4.12", okhttp4_12::emulation),
    OkHttp5 => ("okhttp_5", okhttp5::emulation)

);

/// ======== Emulation impls ========
impl hpx::EmulationFactory for Emulation {
    #[inline]
    fn emulation(self) -> hpx::Emulation {
        EmulationOption::builder()
            .emulation(self)
            .build()
            .emulation()
    }
}

define_enum!(
    /// Represents different operating systems for emulation.
    ///
    /// The `EmulationOS` enum provides variants for different operating systems that can be used
    /// to emulate HTTP requests. Each variant corresponds to a specific operating system.
    ///
    /// # Naming Convention
    ///
    /// The naming convention for the variants follows the pattern `os_name`, where
    /// `os_name` is the name of the operating system (e.g., `windows`, `macos`, `linux`, `android`, `ios`).
    ///
    /// The serialized names of the variants use lowercase letters to represent the operating system names.
    /// For example, `Windows` is serialized as `"windows"`.
    plain,
    EmulationOS, MacOS,
    Windows => "windows",
    MacOS => "macos",
    Linux => "linux",
    Android => "android",
    IOS => "ios"
);

define_enum!(
    /// Represents the platform for emulation.
    ///
    /// Maps to [`EmulationOS`] via [`From<Platform> for EmulationOS`].
    /// Default is `Windows`.
    plain,
    Platform, Windows,
    Windows => "windows",
    MacOS => "macos",
    Linux => "linux",
    Android => "android",
    IOS => "ios"
);

impl Platform {
    /// Returns `true` for mobile platforms (`Android` and `IOS`).
    #[inline]
    pub const fn is_mobile(&self) -> bool {
        matches!(self, Platform::Android | Platform::IOS)
    }

    /// Returns the `sec-ch-ua-platform` header value for this platform.
    #[inline]
    pub const fn sec_ch_ua_platform(self) -> &'static str {
        match self {
            Self::Windows => "\"Windows\"",
            Self::MacOS => "\"macOS\"",
            Self::Linux => "\"Linux\"",
            Self::Android => "\"Android\"",
            Self::IOS => "\"iOS\"",
        }
    }
}

impl From<Platform> for EmulationOS {
    #[inline]
    fn from(platform: Platform) -> Self {
        match platform {
            Platform::Windows => Self::Windows,
            Platform::MacOS => Self::MacOS,
            Platform::Linux => Self::Linux,
            Platform::Android => Self::Android,
            Platform::IOS => Self::IOS,
        }
    }
}

/// ======== EmulationOS impls ========
impl EmulationOS {
    #[inline]
    const fn platform(&self) -> &'static str {
        match self {
            EmulationOS::MacOS => "\"macOS\"",
            EmulationOS::Linux => "\"Linux\"",
            EmulationOS::Windows => "\"Windows\"",
            EmulationOS::Android => "\"Android\"",
            EmulationOS::IOS => "\"iOS\"",
        }
    }

    #[inline]
    const fn is_mobile(&self) -> bool {
        matches!(self, EmulationOS::Android | EmulationOS::IOS)
    }
}

/// Represents the configuration options for emulating a browser and operating system.
///
/// The `EmulationOption` struct allows you to configure various aspects of browser and OS
/// emulation, including the browser version, operating system, and whether to skip certain features
/// like HTTP/2 or headers.
///
/// This struct is typically used to build an `EmulationProvider` that can be applied to HTTP
/// clients for making requests that mimic specific browser and OS configurations.
///
/// # Fields
///
/// - `emulation`: The browser version to emulate. Defaults to `Emulation::default()`.
/// - `emulation_os`: The operating system to emulate. Defaults to `EmulationOS::default()`.
/// - `skip_http2`: Whether to skip HTTP/2 support. Defaults to `false`.
/// - `skip_headers`: Whether to skip adding default headers. Defaults to `false`.
#[derive(Default, Clone, Debug, TypedBuilder)]
pub struct EmulationOption {
    /// The browser version to emulate.
    #[builder(default)]
    emulation: Emulation,

    /// The operating system.
    #[builder(default)]
    emulation_os: EmulationOS,

    /// The platform.
    #[builder(default)]
    platform: Platform,

    /// Whether to skip HTTP/2.
    #[builder(default = false)]
    skip_http2: bool,

    /// Whether to skip headers.
    #[builder(default = false)]
    skip_headers: bool,
}

/// ======== EmulationOption impls ========
impl hpx::EmulationFactory for EmulationOption {
    #[inline]
    fn emulation(mut self) -> hpx::Emulation {
        // ponytail: platform takes precedence over emulation_os when platform
        // is not the default (Windows). This avoids the ambiguity of MacOS being
        // both the default and a valid explicit choice.
        if self.platform != Platform::Windows {
            self.emulation_os = EmulationOS::from(self.platform);
        }
        self.emulation.into_emulation(self)
    }
}

#[cfg(test)]
mod tests {
    use hpx::EmulationFactory;

    use super::*;

    #[test]
    fn platform_is_mobile() {
        assert!(!Platform::Windows.is_mobile());
        assert!(!Platform::MacOS.is_mobile());
        assert!(!Platform::Linux.is_mobile());
        assert!(Platform::Android.is_mobile());
        assert!(Platform::IOS.is_mobile());
    }

    #[test]
    fn platform_to_emulation_os() {
        assert!(matches!(
            EmulationOS::from(Platform::Windows),
            EmulationOS::Windows
        ));
        assert!(matches!(
            EmulationOS::from(Platform::MacOS),
            EmulationOS::MacOS
        ));
        assert!(matches!(
            EmulationOS::from(Platform::Linux),
            EmulationOS::Linux
        ));
        assert!(matches!(
            EmulationOS::from(Platform::Android),
            EmulationOS::Android
        ));
        assert!(matches!(EmulationOS::from(Platform::IOS), EmulationOS::IOS));
    }

    #[test]
    fn emulation_option_builder_with_platform() {
        let option = EmulationOption::builder().platform(Platform::Linux).build();
        assert!(matches!(option.platform, Platform::Linux));
    }

    #[test]
    fn platform_affects_emulation_output() {
        let mut emu = EmulationOption::builder()
            .emulation(Emulation::Chrome147)
            .platform(Platform::Linux)
            .build()
            .emulation();
        let ua = emu
            .headers_mut()
            .get(http::header::USER_AGENT)
            .unwrap()
            .to_str()
            .unwrap();
        assert!(
            ua.contains("Linux"),
            "expected Linux in User-Agent, got: {ua}"
        );
        assert!(
            !ua.contains("Macintosh"),
            "did not expect Macintosh in User-Agent, got: {ua}"
        );
    }

    #[test]
    #[cfg(feature = "emulation-rand")]
    fn variant_count_at_least_120() {
        use strum::VariantArray;
        assert!(
            Emulation::VARIANTS.len() >= 120,
            "Expected at least 120 Emulation variants, found {}",
            Emulation::VARIANTS.len()
        );
    }

    #[test]
    fn platform_sec_ch_ua_platform() {
        assert_eq!(Platform::Linux.sec_ch_ua_platform(), "\"Linux\"");
        assert_eq!(Platform::Windows.sec_ch_ua_platform(), "\"Windows\"");
        assert_eq!(Platform::MacOS.sec_ch_ua_platform(), "\"macOS\"");
        assert_eq!(Platform::Android.sec_ch_ua_platform(), "\"Android\"");
        assert_eq!(Platform::IOS.sec_ch_ua_platform(), "\"iOS\"");
    }

    #[test]
    fn explicit_emulation_os_preserved_when_platform_default() {
        // When platform is default (Windows) but emulation_os is explicitly set,
        // emulation_os should be preserved.
        let mut em = EmulationOption::builder()
            .emulation(Emulation::Chrome147)
            .emulation_os(EmulationOS::Linux)
            .build()
            .emulation();
        let ua = em
            .headers_mut()
            .get(http::header::USER_AGENT)
            .unwrap()
            .to_str()
            .unwrap();
        assert!(
            ua.contains("Linux"),
            "expected Linux in User-Agent when emulation_os=Linux, got: {ua}"
        );
    }
}
