use proptest::prelude::*;
use serde_json::{json, Map, Value};
use shelly::{
    validate_client_message_invariants, validate_server_message_sequence, ClientMessage, Context,
    Event, Html, LiveResult, LiveSession, LiveView, ServerMessage, PROTOCOL_VERSION_V1,
};

fn ident_strategy() -> impl Strategy<Value = String> {
    "[a-zA-Z][a-zA-Z0-9_\\-]{0,15}".prop_map(|value| value.to_string())
}

fn optional_target_strategy() -> impl Strategy<Value = Option<String>> {
    prop::option::of(ident_strategy())
}

fn trace_id_strategy() -> impl Strategy<Value = String> {
    "[0-9a-f]{32}".prop_map(|value| value.to_string())
}

fn span_id_strategy() -> impl Strategy<Value = String> {
    "[0-9a-f]{16}".prop_map(|value| value.to_string())
}

#[derive(Default)]
struct CounterView {
    count: i64,
}

impl LiveView for CounterView {
    fn handle_event(&mut self, event: Event, _ctx: &mut Context) -> LiveResult {
        if event.name == "inc" {
            self.count += 1;
        }
        Ok(())
    }

    fn render(&self) -> Html {
        Html::new(format!("<p>{}</p>", self.count))
    }
}

#[derive(Default)]
struct SometimesFailView {
    count: i64,
}

impl LiveView for SometimesFailView {
    fn handle_event(&mut self, event: Event, _ctx: &mut Context) -> LiveResult {
        if event.name == "fail" {
            return Err(shelly::ShellyError::Event("boom".to_string()));
        }
        if event.name == "inc" {
            self.count += 1;
        }
        Ok(())
    }

    fn render(&self) -> Html {
        Html::new(format!("<p>{}</p>", self.count))
    }
}

proptest! {
    #[test]
    fn client_event_roundtrips_through_json(
        event in ident_strategy(),
        target in optional_target_strategy(),
        value in any::<i64>(),
        metadata_tag in ident_strategy(),
    ) {
        let mut metadata = Map::new();
        metadata.insert("tag".to_string(), Value::String(metadata_tag));

        let message = ClientMessage::Event {
            event,
            target,
            value: json!({"value": value}),
            metadata,
        };

        let encoded = serde_json::to_string(&message).unwrap();
        let decoded: ClientMessage = serde_json::from_str(&encoded).unwrap();
        prop_assert_eq!(decoded, message);
    }

    #[test]
    fn unknown_client_message_type_is_rejected(tag in ident_strategy()) {
        prop_assume!(
            tag != "connect" &&
            tag != "event" &&
            tag != "ping" &&
            tag != "patch_url" &&
            tag != "navigate" &&
            tag != "upload_start" &&
            tag != "upload_chunk" &&
            tag != "upload_complete"
        );

        let decoded = serde_json::from_value::<ClientMessage>(json!({"type": tag}));
        prop_assert!(decoded.is_err());
    }

    #[test]
    fn session_revision_is_monotonic_for_successful_events(events in prop::collection::vec(prop::bool::ANY, 1..64)) {
        let mut session = LiveSession::new(Box::<CounterView>::default(), "root");
        session.mount().unwrap();

        let mut previous_revision = session.revision();
        for should_inc in events {
            let name = if should_inc { "inc" } else { "noop" };
            let messages = session.handle_client_message(ClientMessage::Event {
                event: name.to_string(),
                target: None,
                value: Value::Null,
                metadata: Map::new(),
            });

            prop_assert_eq!(messages.len(), 1);
            let revision = match &messages[0] {
                ServerMessage::Patch { revision, .. } => *revision,
                ServerMessage::Diff { revision, .. } => *revision,
                other => panic!("unexpected message for successful event: {other:?}"),
            };
            prop_assert!(revision > previous_revision);
            previous_revision = revision;
        }

        prop_assert_eq!(session.revision(), previous_revision);
    }

    #[test]
    fn failing_events_never_increment_revision(events in prop::collection::vec(prop::bool::ANY, 1..64)) {
        let mut session = LiveSession::new(Box::<SometimesFailView>::default(), "root");
        session.mount().unwrap();

        let mut expected_revision = 0u64;
        for should_fail in events {
            let name = if should_fail { "fail" } else { "inc" };
            let messages = session.handle_client_message(ClientMessage::Event {
                event: name.to_string(),
                target: None,
                value: Value::Null,
                metadata: Map::new(),
            });

            if should_fail {
                prop_assert_eq!(messages.len(), 1);
                let is_error = matches!(messages[0], ServerMessage::Error { .. });
                prop_assert!(is_error);
            } else {
                expected_revision += 1;
                prop_assert_eq!(messages.len(), 1);
                let revision = match &messages[0] {
                    ServerMessage::Patch { revision, .. } => *revision,
                    ServerMessage::Diff { revision, .. } => *revision,
                    other => panic!("unexpected message for successful event: {other:?}"),
                };
                prop_assert_eq!(revision, expected_revision);
            }
            prop_assert_eq!(session.revision(), expected_revision);
        }
    }

    #[test]
    fn supported_connect_with_valid_trace_fields_passes_invariants(
        trace_id in prop::option::of(trace_id_strategy()),
        span_id in prop::option::of(span_id_strategy()),
        parent_span_id in prop::option::of(span_id_strategy()),
    ) {
        let message = ClientMessage::Connect {
            protocol: PROTOCOL_VERSION_V1.to_string(),
            session_id: Some("sid-1".to_string()),
            last_revision: Some(3),
            resume_token: Some("resume-1".to_string()),
            tenant_id: None,
            trace_id,
            span_id,
            parent_span_id,
            correlation_id: Some("corr-1".to_string()),
            request_id: Some("req-1".to_string()),
        };
        prop_assert!(validate_client_message_invariants(&message).is_ok());
    }

    #[test]
    fn connect_with_malformed_trace_ids_fails_invariants(
        trace_id in "[a-zA-Z0-9]{1,40}",
        span_id in "[a-zA-Z0-9]{1,20}",
        parent_span_id in "[a-zA-Z0-9]{1,20}",
    ) {
        let trace_is_valid = trace_id.len() == 32 && trace_id.chars().all(|char| char.is_ascii_hexdigit());
        let span_is_valid = span_id.len() == 16 && span_id.chars().all(|char| char.is_ascii_hexdigit());
        let parent_is_valid = parent_span_id.len() == 16 && parent_span_id.chars().all(|char| char.is_ascii_hexdigit());
        prop_assume!(!(trace_is_valid && span_is_valid && parent_is_valid));

        let message = ClientMessage::Connect {
            protocol: PROTOCOL_VERSION_V1.to_string(),
            session_id: None,
            last_revision: None,
            resume_token: None,
            tenant_id: None,
            trace_id: Some(trace_id),
            span_id: Some(span_id),
            parent_span_id: Some(parent_span_id),
            correlation_id: None,
            request_id: None,
        };
        let err = validate_client_message_invariants(&message).expect_err("invalid ids must fail");
        prop_assert!(
            err.code == "invalid_hex_id_length" || err.code == "invalid_hex_id_charset",
            "unexpected invariant violation code: {}",
            err.code
        );
    }

    #[test]
    fn strictly_increasing_render_revisions_pass_sequence_validation(
        revisions in prop::collection::vec(1u16..512u16, 1..32)
    ) {
        let mut ordered = revisions
            .into_iter()
            .map(u64::from)
            .collect::<Vec<_>>();
        ordered.sort_unstable();
        ordered.dedup();
        prop_assume!(!ordered.is_empty());

        let mut transcript = Vec::with_capacity(ordered.len() + 1);
        transcript.push(ServerMessage::Hello {
            session_id: "sid-seq".to_string(),
            target: "root".to_string(),
            revision: 0,
            protocol: PROTOCOL_VERSION_V1.to_string(),
            server_revision: None,
            resume_status: None,
            resume_reason: None,
            resume_token: None,
            resume_expires_in_ms: None,
        });
        for (idx, revision) in ordered.iter().enumerate() {
            if idx % 2 == 0 {
                transcript.push(ServerMessage::Patch {
                    target: "root".to_string(),
                    html: "<p>ok</p>".to_string(),
                    revision: *revision,
                });
            } else {
                transcript.push(ServerMessage::Diff {
                    target: "root".to_string(),
                    revision: *revision,
                    slots: vec![],
                });
            }
        }

        prop_assert!(validate_server_message_sequence(&transcript).is_ok());
    }
}
