use parking_lot::RwLockReadGuard;

use crate::{
    datafile::rule::RuleType,
    storage::visitor_data::VisitorData,
    tracking::{
        data_adding_context::DataAddingContext,
        trackable::{Trackable, TrackingState},
    },
};

pub(crate) fn select_all_unsent_trackable_data<'a>(
    context: &mut DataAddingContext<'a>,
    visitor_data: &'a RwLockReadGuard<'a, VisitorData>,
) {
    add_unsent_opt(context, visitor_data.device.r().get());
    add_unsent_opt(context, visitor_data.browser.r().get());
    add_unsent_opt(context, visitor_data.operating_system.r().get());
    add_unsent_opt(context, visitor_data.geolocation.r().get().map(|g| g.as_ref()));
    add_unsent_opt(context, visitor_data.visitor_visits.r().get().map(|vv| vv.as_ref()));
    for cd in filter_unsent(visitor_data.custom_data_map.r().iter()) {
        context.add_data(cd);
    }
    for pv in filter_unsent(visitor_data.page_view_visits.r().iter().map(|pvv| pvv.page_view())) {
        context.add_data(pv);
    }
    for av in filter_unsent(visitor_data.assigned_variations.r().iter()) {
        context.add_data(av);
    }
    for c in filter_unsent(visitor_data.conversions.iter()) {
        context.add_data(c);
    }
}

pub(crate) fn select_technical_unsent_trackable_data<'a>(
    context: &mut DataAddingContext<'a>,
    visitor_data: &'a RwLockReadGuard<'a, VisitorData>,
) {
    for av in filter_unsent(visitor_data.assigned_variations.r().iter()) {
        if av.rule_type == RuleType::TargetedDelivery {
            context.add_data(av);
        }
    }
    for c in filter_unsent(visitor_data.conversions.iter()) {
        context.add_data(c);
    }
}

#[inline]
fn add_unsent_opt<'a, T: Trackable>(context: &mut DataAddingContext<'a>, data: Option<&'a T>) {
    if let Some(data) = data {
        if data.state_cell().state() == TrackingState::Unsent {
            context.add_data(data);
        }
    }
}

#[inline]
fn filter_unsent<'a, T: Trackable + 'a>(it: impl Iterator<Item = &'a T>) -> impl Iterator<Item = &'a T> {
    it.filter(|t| t.state_cell().state() == TrackingState::Unsent)
}

// MARK: - Tests

#[cfg(test)]
mod tests {
    use std::{
        sync::Arc,
        time::{Duration, SystemTime},
    };

    use crate::{
        data::{
            BaseData, Browser, BrowserKind, Conversion, CustomData, Device, DeviceKind, OperatingSystem,
            OperatingSystemKind, PageView, assigned_variation::AssignedVariation, geolocation::GeolocationBuilder,
            visitor_visits::VisitorVisits,
        },
        storage::visitor::Visitor,
        tracking::tracking_data::TrackingData,
    };

    use super::*;
    use rstest::rstest;

    fn mark_as<T: Trackable>(state: TrackingState, data: T) -> T {
        data.state_cell().change_state(state);
        return data;
    }

    fn make_data(state: TrackingState) -> Vec<BaseData> {
        vec![
            BaseData::AssignedVariation(mark_as(state, AssignedVariation::new(1, 10, RuleType::Unknown))),
            BaseData::AssignedVariation(mark_as(state, AssignedVariation::new(2, 20, RuleType::Experimentation))),
            BaseData::AssignedVariation(mark_as(state, AssignedVariation::new(3, 30, RuleType::TargetedDelivery))),
            BaseData::Browser(mark_as(state, Browser::new(BrowserKind::Chrome, None))),
            BaseData::Conversion(mark_as(state, Conversion::new(1))),
            BaseData::CustomData(mark_as(state, CustomData::new_with_index(1, vec!["v1".to_owned()]))),
            BaseData::Device(mark_as(state, Device::new(DeviceKind::Desktop))),
            BaseData::Geolocation(mark_as(state, GeolocationBuilder::default().country("co").build().unwrap())),
            BaseData::OperatingSystem(mark_as(state, OperatingSystem::new(OperatingSystemKind::Linux))),
            BaseData::PageView(mark_as(state, PageView::new_with_url("url1"))),
            BaseData::VisitorVisits(mark_as(
                state,
                VisitorVisits::new(6, vec![], SystemTime::UNIX_EPOCH, Duration::ZERO),
            )),
        ]
    }

    #[rstest]
    fn test_select_all_unsent_trackable_data_order() {
        // arrange
        const VISITOR_CODE_PARAM: &str = "vcparam1=vc123";
        let mut result = TrackingData::new();
        let mut context = DataAddingContext::new(&mut result, VISITOR_CODE_PARAM, None);
        let visitor = Visitor::new();
        visitor.add_data(make_data(TrackingState::Unsent).into_iter(), true);
        let vdata = visitor.rdata();
        // act
        select_all_unsent_trackable_data(&mut context, &vdata);
        // assert
        #[rustfmt::skip]
        let expected = vec![
            "staticData", "staticData", "staticData", "geolocation", "staticData", "customData", "page",
            "experiment", "experiment", "experiment", "conversion",
        ];
        assert_eq!(expected.len(), result.tracking_lines.len());
        for (e, a) in expected.iter().zip(result.tracking_lines.iter()) {
            assert!(a.contains(e), "'{e}' in '{a}'");
        }
    }

    #[rstest]
    fn test_select_required_unsent_trackable_data_order() {
        // arrange
        const VISITOR_CODE_PARAM: &str = "vcparam2=vc234";
        let mut result = TrackingData::new();
        let mut context = DataAddingContext::new(&mut result, VISITOR_CODE_PARAM, None);
        let visitor = Visitor::new();
        visitor.add_data(make_data(TrackingState::Unsent).into_iter(), true);
        let vdata = visitor.rdata();
        // act
        select_technical_unsent_trackable_data(&mut context, &vdata);
        // assert
        let mut actual = result.unsent_trackable_data.iter();
        #[rustfmt::skip]
        assert!(Arc::ptr_eq(
            vdata.assigned_variations.r().iter()
                .find(|av| av.rule_type == RuleType::TargetedDelivery)
                .unwrap().state_cell(),
            actual.next().unwrap()
        ));
        assert!(Arc::ptr_eq(vdata.conversions.iter().next().unwrap().state_cell(), actual.next().unwrap()));
        assert!(actual.next().is_none());
    }

    #[rstest]
    fn test_select_unsent_trackable_skips_not_unsent(
        #[values(TrackingState::Transmitting, TrackingState::Sent)] state: TrackingState,
        #[values(select_all_unsent_trackable_data, select_technical_unsent_trackable_data)]
        selection_func: for<'a> fn(&'a mut DataAddingContext<'a>, &'a RwLockReadGuard<'a, VisitorData>),
    ) {
        // arrange
        const VISITOR_CODE_PARAM: &str = "vcparam3=vc345";
        let mut result = TrackingData::new();
        let mut context = DataAddingContext::new(&mut result, VISITOR_CODE_PARAM, None);
        let visitor = Visitor::new();
        visitor.add_data(make_data(state).into_iter(), true);
        let vdata = visitor.rdata();
        // act
        selection_func(&mut context, &vdata);
        // assert
        assert!(result.unsent_trackable_data.is_empty());
    }
}
