Skip to main content

this/links/
handlers.rs

1//! HTTP handlers for link operations
2//!
3//! This module provides generic handlers that work with any entity types.
4//! All handlers are completely entity-agnostic.
5
6use axum::{
7    Json,
8    extract::{Path, Query, State},
9    http::StatusCode,
10    response::{IntoResponse, Response},
11};
12use chrono::{DateTime, Utc};
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::HashMap;
16use std::sync::Arc;
17use uuid::Uuid;
18
19use crate::config::LinksConfig;
20use crate::core::events::{EventBus, FrameworkEvent, LinkEvent};
21use crate::core::extractors::{
22    DirectLinkExtractor, ExtractorError, LinkExtractor, RecursiveLinkExtractor,
23};
24use crate::core::{
25    EntityCreator, EntityFetcher, LinkDefinition, LinkService,
26    link::LinkEntity,
27    query::{PaginationMeta, QueryParams},
28};
29use crate::links::registry::{LinkDirection, LinkRouteRegistry};
30
31/// Application state shared across handlers
32#[derive(Clone)]
33pub struct AppState {
34    pub link_service: Arc<dyn LinkService>,
35    pub config: Arc<LinksConfig>,
36    pub registry: Arc<LinkRouteRegistry>,
37    /// Entity fetchers for enriching links with full entity data
38    pub entity_fetchers: Arc<HashMap<String, Arc<dyn EntityFetcher>>>,
39    /// Entity creators for creating new entities with automatic linking
40    pub entity_creators: Arc<HashMap<String, Arc<dyn EntityCreator>>>,
41    /// Optional event bus for publishing real-time events
42    pub event_bus: Option<Arc<EventBus>>,
43}
44
45impl AppState {
46    /// Publish an event to the event bus (if configured)
47    ///
48    /// This is non-blocking and fire-and-forget. If there are no subscribers
49    /// or no event bus configured, the event is silently dropped.
50    pub fn publish_event(&self, event: FrameworkEvent) {
51        if let Some(ref bus) = self.event_bus {
52            bus.publish(event);
53        }
54    }
55
56    /// Get the authorization policy for a link operation
57    pub fn get_link_auth_policy(
58        link_definition: &LinkDefinition,
59        operation: &str,
60    ) -> Option<String> {
61        link_definition.auth.as_ref().map(|auth| match operation {
62            "list" => auth.list.clone(),
63            "get" => auth.get.clone(),
64            "create" => auth.create.clone(),
65            "update" => auth.update.clone(),
66            "delete" => auth.delete.clone(),
67            _ => "authenticated".to_string(),
68        })
69    }
70}
71
72/// Response for list links endpoint
73#[derive(Debug, Serialize)]
74pub struct ListLinksResponse {
75    pub links: Vec<LinkEntity>,
76    pub count: usize,
77    pub link_type: String,
78    pub direction: String,
79    pub description: Option<String>,
80}
81
82/// Link with full entity data instead of just references
83#[derive(Debug, Serialize)]
84pub struct EnrichedLink {
85    /// Unique identifier for this link
86    pub id: Uuid,
87
88    /// Entity type
89    #[serde(rename = "type")]
90    pub entity_type: String,
91
92    /// The type of relationship (e.g., "has_invoice", "payment")
93    pub link_type: String,
94
95    /// Source entity ID
96    pub source_id: Uuid,
97
98    /// Target entity ID
99    pub target_id: Uuid,
100
101    /// Full source entity as JSON (omitted when querying from source)
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub source: Option<serde_json::Value>,
104
105    /// Full target entity as JSON (omitted when querying from target)
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub target: Option<serde_json::Value>,
108
109    /// Optional metadata for the relationship
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub metadata: Option<serde_json::Value>,
112
113    /// When this link was created
114    pub created_at: DateTime<Utc>,
115
116    /// When this link was last updated
117    pub updated_at: DateTime<Utc>,
118
119    /// Status
120    pub status: String,
121}
122
123/// Response for enriched list links endpoint (legacy, without pagination)
124#[derive(Debug, Serialize)]
125pub struct EnrichedListLinksResponse {
126    pub links: Vec<EnrichedLink>,
127    pub count: usize,
128    pub link_type: String,
129    pub direction: String,
130    pub description: Option<String>,
131}
132
133/// Paginated response for enriched list links endpoint
134#[derive(Debug, Serialize)]
135pub struct PaginatedEnrichedLinksResponse {
136    pub data: Vec<EnrichedLink>,
137    pub pagination: PaginationMeta,
138    pub link_type: String,
139    pub direction: String,
140    pub description: Option<String>,
141}
142
143/// Request body for creating a link between existing entities
144#[derive(Debug, Deserialize)]
145pub struct CreateLinkRequest {
146    pub metadata: Option<serde_json::Value>,
147}
148
149/// Request body for creating a new linked entity
150#[derive(Debug, Deserialize)]
151pub struct CreateLinkedEntityRequest {
152    pub entity: serde_json::Value,
153    pub metadata: Option<serde_json::Value>,
154}
155
156/// Context for link enrichment
157#[derive(Debug, Clone, Copy)]
158pub enum EnrichmentContext {
159    /// Query from source entity - only target entities are included
160    FromSource,
161    /// Query from target entity - only source entities are included
162    FromTarget,
163    /// Direct link access - both source and target entities are included
164    DirectLink,
165}
166
167/// List links using named routes (forward or reverse) - WITH PAGINATION
168///
169/// GET /{entity_type}/{entity_id}/{route_name}
170pub async fn list_links(
171    State(state): State<AppState>,
172    Path((entity_type_plural, entity_id, route_name)): Path<(String, Uuid, String)>,
173    Query(params): Query<QueryParams>,
174) -> Result<Json<PaginatedEnrichedLinksResponse>, ExtractorError> {
175    let extractor = LinkExtractor::from_path_and_registry(
176        (entity_type_plural, entity_id, route_name),
177        &state.registry,
178        &state.config,
179    )?;
180
181    // Query links based on direction
182    let links = match extractor.direction {
183        LinkDirection::Forward => state
184            .link_service
185            .find_by_source(
186                &extractor.entity_id,
187                Some(&extractor.link_definition.link_type),
188                Some(&extractor.link_definition.target_type),
189            )
190            .await
191            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
192        LinkDirection::Reverse => state
193            .link_service
194            .find_by_target(
195                &extractor.entity_id,
196                Some(&extractor.link_definition.link_type),
197                Some(&extractor.link_definition.source_type),
198            )
199            .await
200            .map_err(|e| ExtractorError::JsonError(e.to_string()))?,
201    };
202
203    // Determine enrichment context based on direction
204    let context = match extractor.direction {
205        LinkDirection::Forward => EnrichmentContext::FromSource,
206        LinkDirection::Reverse => EnrichmentContext::FromTarget,
207    };
208
209    // Enrich ALL links with full entity data first
210    let mut all_enriched =
211        enrich_links_with_entities(&state, links, context, &extractor.link_definition).await?;
212
213    // Apply filters if provided
214    if let Some(filter_value) = params.filter_value() {
215        all_enriched = apply_link_filters(all_enriched, &filter_value);
216    }
217
218    let total = all_enriched.len();
219
220    // Apply pagination (ALWAYS paginate for links)
221    let page = params.page();
222    let limit = params.limit();
223    let start = (page - 1) * limit;
224
225    let paginated_links: Vec<EnrichedLink> =
226        all_enriched.into_iter().skip(start).take(limit).collect();
227
228    Ok(Json(PaginatedEnrichedLinksResponse {
229        data: paginated_links,
230        pagination: PaginationMeta::new(page, limit, total),
231        link_type: extractor.link_definition.link_type,
232        direction: format!("{:?}", extractor.direction),
233        description: extractor.link_definition.description,
234    }))
235}
236
237/// Helper function to enrich links with full entity data
238async fn enrich_links_with_entities(
239    state: &AppState,
240    links: Vec<LinkEntity>,
241    context: EnrichmentContext,
242    link_definition: &LinkDefinition,
243) -> Result<Vec<EnrichedLink>, ExtractorError> {
244    let mut enriched = Vec::new();
245
246    for link in links {
247        // Fetch source entity only if needed
248        let source_entity = match context {
249            EnrichmentContext::FromSource => None,
250            EnrichmentContext::FromTarget | EnrichmentContext::DirectLink => {
251                // Fetch source entity using the type from link definition
252                fetch_entity_by_type(state, &link_definition.source_type, &link.source_id)
253                    .await
254                    .ok()
255            }
256        };
257
258        // Fetch target entity only if needed
259        let target_entity = match context {
260            EnrichmentContext::FromTarget => None,
261            EnrichmentContext::FromSource | EnrichmentContext::DirectLink => {
262                // Fetch target entity using the type from link definition
263                fetch_entity_by_type(state, &link_definition.target_type, &link.target_id)
264                    .await
265                    .ok()
266            }
267        };
268
269        enriched.push(EnrichedLink {
270            id: link.id,
271            entity_type: link.entity_type,
272            link_type: link.link_type,
273            source_id: link.source_id,
274            target_id: link.target_id,
275            source: source_entity,
276            target: target_entity,
277            metadata: link.metadata,
278            created_at: link.created_at,
279            updated_at: link.updated_at,
280            status: link.status,
281        });
282    }
283
284    Ok(enriched)
285}
286
287/// Fetch an entity dynamically by type
288async fn fetch_entity_by_type(
289    state: &AppState,
290    entity_type: &str,
291    entity_id: &Uuid,
292) -> Result<serde_json::Value, ExtractorError> {
293    let fetcher = state.entity_fetchers.get(entity_type).ok_or_else(|| {
294        ExtractorError::JsonError(format!(
295            "No entity fetcher registered for type: {}",
296            entity_type
297        ))
298    })?;
299
300    fetcher
301        .fetch_as_json(entity_id)
302        .await
303        .map_err(|e| ExtractorError::JsonError(format!("Failed to fetch entity: {}", e)))
304}
305
306/// Apply filtering to enriched links based on query parameters
307///
308/// Supports filtering on:
309/// - link fields (id, link_type, source_id, target_id, status, metadata)
310/// - nested entity fields (source.*, target.*)
311fn apply_link_filters(enriched_links: Vec<EnrichedLink>, filter: &Value) -> Vec<EnrichedLink> {
312    if filter.is_null() || !filter.is_object() {
313        return enriched_links;
314    }
315
316    let filter_obj = filter.as_object().unwrap();
317
318    enriched_links
319        .into_iter()
320        .filter(|link| {
321            let mut matches = true;
322
323            // Convert link to JSON for easy filtering
324            let link_json = match serde_json::to_value(link) {
325                Ok(v) => v,
326                Err(_) => return false,
327            };
328
329            for (key, value) in filter_obj.iter() {
330                // Check if the field exists in the link or in nested entities
331                let field_value = get_nested_value(&link_json, key);
332
333                match field_value {
334                    Some(field_val) => {
335                        // Simple equality match for now
336                        if field_val != *value {
337                            matches = false;
338                            break;
339                        }
340                    }
341                    None => {
342                        matches = false;
343                        break;
344                    }
345                }
346            }
347
348            matches
349        })
350        .collect()
351}
352
353/// Get a nested value from JSON using dot notation
354/// E.g., "source.name" or "target.amount"
355fn get_nested_value(json: &Value, key: &str) -> Option<Value> {
356    let parts: Vec<&str> = key.split('.').collect();
357
358    match parts.len() {
359        1 => json.get(key).cloned(),
360        2 => {
361            let (parent, child) = (parts[0], parts[1]);
362            json.get(parent).and_then(|v| v.get(child)).cloned()
363        }
364        _ => None,
365    }
366}
367
368/// Get a specific link by ID
369///
370/// GET /links/{link_id}
371pub async fn get_link(
372    State(state): State<AppState>,
373    Path(link_id): Path<Uuid>,
374) -> Result<Response, ExtractorError> {
375    let link = state
376        .link_service
377        .get(&link_id)
378        .await
379        .map_err(|e| ExtractorError::JsonError(e.to_string()))?
380        .ok_or(ExtractorError::LinkNotFound)?;
381
382    // Find the link definition from config
383    let link_definition = state
384        .config
385        .links
386        .iter()
387        .find(|def| def.link_type == link.link_type)
388        .ok_or_else(|| {
389            ExtractorError::JsonError(format!(
390                "No link definition found for link_type: {}",
391                link.link_type
392            ))
393        })?;
394
395    // Enrich with both source and target entities
396    let enriched_links = enrich_links_with_entities(
397        &state,
398        vec![link],
399        EnrichmentContext::DirectLink,
400        link_definition,
401    )
402    .await?;
403
404    let enriched_link = enriched_links
405        .into_iter()
406        .next()
407        .ok_or(ExtractorError::LinkNotFound)?;
408
409    Ok(Json(enriched_link).into_response())
410}
411
412/// Get a specific link by source, route_name, and target
413///
414/// GET /{source_type}/{source_id}/{route_name}/{target_id}
415pub async fn get_link_by_route(
416    State(state): State<AppState>,
417    Path((source_type_plural, source_id, route_name, target_id)): Path<(
418        String,
419        Uuid,
420        String,
421        Uuid,
422    )>,
423) -> Result<Response, ExtractorError> {
424    let extractor = DirectLinkExtractor::from_path(
425        (source_type_plural, source_id, route_name, target_id),
426        &state.registry,
427        &state.config,
428    )?;
429
430    // Find the specific link based on direction
431    let existing_links = match extractor.direction {
432        LinkDirection::Forward => {
433            // Forward: search by source_id in URL
434            state
435                .link_service
436                .find_by_source(
437                    &extractor.source_id,
438                    Some(&extractor.link_definition.link_type),
439                    Some(&extractor.target_type),
440                )
441                .await
442                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
443        }
444        LinkDirection::Reverse => {
445            // Reverse: search by target_id in URL (which is the actual source in DB)
446            state
447                .link_service
448                .find_by_source(
449                    &extractor.target_id,
450                    Some(&extractor.link_definition.link_type),
451                    Some(&extractor.source_type),
452                )
453                .await
454                .map_err(|e| ExtractorError::JsonError(e.to_string()))?
455        }
456    };
457
458    let link = existing_links
459        .into_iter()
460        .find(|link| match extractor.direction {
461            LinkDirection::Forward => link.target_id == extractor.target_id,
462            LinkDirection::Reverse => link.target_id == extractor.source_id,
463        })
464        .ok_or(ExtractorError::LinkNotFound)?;
465
466    // Enrich with both source and target entities
467    let enriched_links = enrich_links_with_entities(
468        &state,
469        vec![link],
470        EnrichmentContext::DirectLink,
471        &extractor.link_definition,
472    )
473    .await?;
474
475    let enriched_link = enriched_links
476        .into_iter()
477        .next()
478        .ok_or(ExtractorError::LinkNotFound)?;
479
480    Ok(Json(enriched_link).into_response())
481}
482
483/// Create a link between two existing entities
484///
485/// POST /{source_type}/{source_id}/{route_name}/{target_id}
486/// Body: { "metadata": {...} }
487pub async fn create_link(
488    State(state): State<AppState>,
489    Path((source_type_plural, source_id, route_name, target_id)): Path<(
490        String,
491        Uuid,
492        String,
493        Uuid,
494    )>,
495    Json(payload): Json<CreateLinkRequest>,
496) -> Result<Response, ExtractorError> {
497    let extractor = DirectLinkExtractor::from_path(
498        (source_type_plural, source_id, route_name, target_id),
499        &state.registry,
500        &state.config,
501    )?;
502
503    // Create the link between existing entities
504    let link = LinkEntity::new(
505        extractor.link_definition.link_type,
506        extractor.source_id,
507        extractor.target_id,
508        payload.metadata,
509    );
510
511    let created_link = state
512        .link_service
513        .create(link)
514        .await
515        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
516
517    // Emit link created event
518    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
519        link_type: created_link.link_type.clone(),
520        link_id: created_link.id,
521        source_id: created_link.source_id,
522        target_id: created_link.target_id,
523        metadata: created_link.metadata.clone(),
524    }));
525
526    Ok((StatusCode::CREATED, Json(created_link)).into_response())
527}
528
529/// Create a new entity and link it to the source
530///
531/// POST /{source_type}/{source_id}/{route_name}
532/// Body: { "entity": {...entity fields...}, "metadata": {...link metadata...} }
533pub async fn create_linked_entity(
534    State(state): State<AppState>,
535    Path((source_type_plural, source_id, route_name)): Path<(String, Uuid, String)>,
536    Json(payload): Json<CreateLinkedEntityRequest>,
537) -> Result<Response, ExtractorError> {
538    let extractor = LinkExtractor::from_path_and_registry(
539        (source_type_plural.clone(), source_id, route_name.clone()),
540        &state.registry,
541        &state.config,
542    )?;
543
544    // Determine source and target based on direction
545    let (source_entity_id, target_entity_type) = match extractor.direction {
546        LinkDirection::Forward => {
547            // Forward: source is the entity in the URL, target is the new entity
548            (extractor.entity_id, &extractor.link_definition.target_type)
549        }
550        LinkDirection::Reverse => {
551            // Reverse: target is the entity in the URL, source is the new entity
552            (extractor.entity_id, &extractor.link_definition.source_type)
553        }
554    };
555
556    // Get the entity creator for the target type
557    let entity_creator = state
558        .entity_creators
559        .get(target_entity_type)
560        .ok_or_else(|| {
561            ExtractorError::JsonError(format!(
562                "No entity creator registered for type: {}",
563                target_entity_type
564            ))
565        })?;
566
567    // Create the new entity
568    let created_entity = entity_creator
569        .create_from_json(payload.entity)
570        .await
571        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
572
573    // Extract the ID from the created entity
574    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
575        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
576    })?;
577    let target_entity_id = Uuid::parse_str(target_entity_id)
578        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
579
580    // Create the link based on direction
581    let link = match extractor.direction {
582        LinkDirection::Forward => {
583            // Forward: source -> target (new entity)
584            LinkEntity::new(
585                extractor.link_definition.link_type,
586                source_entity_id,
587                target_entity_id,
588                payload.metadata,
589            )
590        }
591        LinkDirection::Reverse => {
592            // Reverse: source (new entity) -> target
593            LinkEntity::new(
594                extractor.link_definition.link_type,
595                target_entity_id,
596                source_entity_id,
597                payload.metadata,
598            )
599        }
600    };
601
602    let created_link = state
603        .link_service
604        .create(link)
605        .await
606        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
607
608    // Emit entity created event
609    state.publish_event(FrameworkEvent::Entity(
610        crate::core::events::EntityEvent::Created {
611            entity_type: target_entity_type.clone(),
612            entity_id: target_entity_id,
613            data: created_entity.clone(),
614        },
615    ));
616
617    // Emit link created event
618    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
619        link_type: created_link.link_type.clone(),
620        link_id: created_link.id,
621        source_id: created_link.source_id,
622        target_id: created_link.target_id,
623        metadata: created_link.metadata.clone(),
624    }));
625
626    // Return both the created entity and the link
627    let response = serde_json::json!({
628        "entity": created_entity,
629        "link": created_link,
630    });
631
632    Ok((StatusCode::CREATED, Json(response)).into_response())
633}
634
635/// Update a link's metadata using route name
636///
637/// PUT/PATCH /{source_type}/{source_id}/{route_name}/{target_id}
638pub async fn update_link(
639    State(state): State<AppState>,
640    Path((source_type_plural, source_id, route_name, target_id)): Path<(
641        String,
642        Uuid,
643        String,
644        Uuid,
645    )>,
646    Json(payload): Json<CreateLinkRequest>,
647) -> Result<Response, ExtractorError> {
648    let extractor = DirectLinkExtractor::from_path(
649        (source_type_plural, source_id, route_name, target_id),
650        &state.registry,
651        &state.config,
652    )?;
653
654    // Find the existing link
655    let existing_links = state
656        .link_service
657        .find_by_source(
658            &extractor.source_id,
659            Some(&extractor.link_definition.link_type),
660            Some(&extractor.target_type),
661        )
662        .await
663        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
664
665    let mut existing_link = existing_links
666        .into_iter()
667        .find(|link| link.target_id == extractor.target_id)
668        .ok_or_else(|| ExtractorError::RouteNotFound("Link not found".to_string()))?;
669
670    // Update metadata
671    existing_link.metadata = payload.metadata;
672    existing_link.touch();
673
674    // Save the updated link
675    let link_id = existing_link.id;
676    let updated_link = state
677        .link_service
678        .update(&link_id, existing_link)
679        .await
680        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
681
682    Ok(Json(updated_link).into_response())
683}
684
685/// Delete a link using route name
686///
687/// DELETE /{source_type}/{source_id}/{route_name}/{target_id}
688pub async fn delete_link(
689    State(state): State<AppState>,
690    Path((source_type_plural, source_id, route_name, target_id)): Path<(
691        String,
692        Uuid,
693        String,
694        Uuid,
695    )>,
696) -> Result<Response, ExtractorError> {
697    let extractor = DirectLinkExtractor::from_path(
698        (source_type_plural, source_id, route_name, target_id),
699        &state.registry,
700        &state.config,
701    )?;
702
703    // Find the existing link first
704    let existing_links = state
705        .link_service
706        .find_by_source(
707            &extractor.source_id,
708            Some(&extractor.link_definition.link_type),
709            Some(&extractor.target_type),
710        )
711        .await
712        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
713
714    let existing_link = existing_links
715        .into_iter()
716        .find(|link| link.target_id == extractor.target_id)
717        .ok_or(ExtractorError::LinkNotFound)?;
718
719    // Delete the link by its ID
720    state
721        .link_service
722        .delete(&existing_link.id)
723        .await
724        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
725
726    // Emit link deleted event
727    state.publish_event(FrameworkEvent::Link(LinkEvent::Deleted {
728        link_type: existing_link.link_type.clone(),
729        link_id: existing_link.id,
730        source_id: existing_link.source_id,
731        target_id: existing_link.target_id,
732    }));
733
734    Ok(StatusCode::NO_CONTENT.into_response())
735}
736
737/// Response for introspection endpoint
738#[derive(Debug, Serialize)]
739pub struct IntrospectionResponse {
740    pub entity_type: String,
741    pub entity_id: Uuid,
742    pub available_routes: Vec<RouteDescription>,
743}
744
745/// Description of an available route
746#[derive(Debug, Serialize)]
747pub struct RouteDescription {
748    pub path: String,
749    pub method: String,
750    pub link_type: String,
751    pub direction: String,
752    pub connected_to: String,
753    pub description: Option<String>,
754}
755
756/// Introspection: List all available link routes for an entity
757///
758/// GET /{entity_type}/{entity_id}/links
759pub async fn list_available_links(
760    State(state): State<AppState>,
761    Path((entity_type_plural, entity_id)): Path<(String, Uuid)>,
762) -> Result<Json<IntrospectionResponse>, ExtractorError> {
763    // Convert plural to singular
764    let entity_type = state
765        .config
766        .entities
767        .iter()
768        .find(|e| e.plural == entity_type_plural)
769        .map(|e| e.singular.clone())
770        .unwrap_or_else(|| entity_type_plural.clone());
771
772    // Get all routes for this entity type
773    let routes = state.registry.list_routes_for_entity(&entity_type);
774
775    let available_routes = routes
776        .iter()
777        .map(|r| RouteDescription {
778            path: format!("/{}/{}/{}", entity_type_plural, entity_id, r.route_name),
779            method: "GET".to_string(),
780            link_type: r.link_type.clone(),
781            direction: format!("{:?}", r.direction),
782            connected_to: r.connected_to.clone(),
783            description: r.description.clone(),
784        })
785        .collect();
786
787    Ok(Json(IntrospectionResponse {
788        entity_type,
789        entity_id,
790        available_routes,
791    }))
792}
793
794/// Handler générique pour GET sur chemins imbriqués illimités
795///
796/// Supporte des chemins comme:
797/// - GET /users/123/invoices/456/orders (liste les orders)
798/// - GET /users/123/invoices/456/orders/789 (get un order spécifique)
799pub async fn handle_nested_path_get(
800    State(state): State<AppState>,
801    Path(path): Path<String>,
802    Query(params): Query<QueryParams>,
803) -> Result<Json<serde_json::Value>, ExtractorError> {
804    // Parser le path en segments
805    let segments: Vec<String> = path
806        .trim_matches('/')
807        .split('/')
808        .map(|s| s.to_string())
809        .collect();
810
811    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
812    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
813    if segments.len() < 5 {
814        return Err(ExtractorError::InvalidPath);
815    }
816
817    // Utiliser l'extracteur récursif
818    let extractor =
819        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
820
821    // Si is_list, récupérer les liens depuis la dernière entité
822    if extractor.is_list {
823        // Valider toute la chaîne de liens avant de retourner les résultats
824        // Pour chaque segment avec un link_definition, vérifier que le lien existe
825        // SAUF pour le dernier segment si c'est une liste (ID = Uuid::nil())
826
827        use crate::links::registry::LinkDirection;
828
829        // VALIDATION COMPLÈTE DE LA CHAÎNE
830        for i in 0..extractor.chain.len() - 1 {
831            let current = &extractor.chain[i];
832            let next = &extractor.chain[i + 1];
833
834            // Si next.entity_id est Uuid::nil(), c'est une liste finale, on ne valide pas ce lien
835            if next.entity_id.is_nil() {
836                continue;
837            }
838
839            // Cas 1: Le segment a un link_definition → validation normale
840            if let Some(link_def) = &current.link_definition {
841                let link_exists = match current.link_direction {
842                    Some(LinkDirection::Forward) => {
843                        // Forward: current est la source, next est le target
844                        let links = state
845                            .link_service
846                            .find_by_source(
847                                &current.entity_id,
848                                Some(&link_def.link_type),
849                                Some(&link_def.target_type),
850                            )
851                            .await
852                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
853                        links.iter().any(|l| l.target_id == next.entity_id)
854                    }
855                    Some(LinkDirection::Reverse) => {
856                        // Reverse: current est le target, next est la source
857                        let links = state
858                            .link_service
859                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
860                            .await
861                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
862                        links.iter().any(|l| l.source_id == next.entity_id)
863                    }
864                    None => {
865                        return Err(ExtractorError::InvalidPath);
866                    }
867                };
868
869                if !link_exists {
870                    return Err(ExtractorError::LinkNotFound);
871                }
872            }
873            // Cas 2: Premier segment sans link_definition mais next a un link_definition
874            // → C'est le début d'une chaîne, on doit vérifier que current est lié à next
875            else if let Some(next_link_def) = &next.link_definition {
876                let link_exists = match next.link_direction {
877                    Some(LinkDirection::Forward) => {
878                        // Forward depuis current: current → next
879                        let links = state
880                            .link_service
881                            .find_by_source(
882                                &current.entity_id,
883                                Some(&next_link_def.link_type),
884                                Some(&next_link_def.target_type),
885                            )
886                            .await
887                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
888                        links.iter().any(|l| l.target_id == next.entity_id)
889                    }
890                    Some(LinkDirection::Reverse) => {
891                        // Reverse depuis current: current ← next (donc next est source)
892                        let links = state
893                            .link_service
894                            .find_by_target(
895                                &current.entity_id,
896                                None,
897                                Some(&next_link_def.link_type),
898                            )
899                            .await
900                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
901                        links.iter().any(|l| l.source_id == next.entity_id)
902                    }
903                    None => {
904                        return Err(ExtractorError::InvalidPath);
905                    }
906                };
907
908                if !link_exists {
909                    return Err(ExtractorError::LinkNotFound);
910                }
911            }
912        }
913
914        // Toute la chaîne est valide, récupérer les liens finaux
915        if let Some(link_def) = extractor.final_link_def() {
916            // Pour une liste, on veut l'ID du segment pénultième (celui qui a le lien)
917            let penultimate = extractor
918                .penultimate_segment()
919                .ok_or(ExtractorError::InvalidPath)?;
920            let entity_id = penultimate.entity_id;
921
922            use crate::links::registry::LinkDirection;
923
924            // Récupérer les liens selon la direction
925            let (links, enrichment_context) = match penultimate.link_direction {
926                Some(LinkDirection::Forward) => {
927                    // Forward: entity_id est la source
928                    let links = state
929                        .link_service
930                        .find_by_source(
931                            &entity_id,
932                            Some(&link_def.link_type),
933                            Some(&link_def.target_type),
934                        )
935                        .await
936                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
937                    (links, EnrichmentContext::FromSource)
938                }
939                Some(LinkDirection::Reverse) => {
940                    // Reverse: entity_id est le target, on cherche les sources
941                    let links = state
942                        .link_service
943                        .find_by_target(&entity_id, None, Some(&link_def.link_type))
944                        .await
945                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
946                    (links, EnrichmentContext::FromTarget)
947                }
948                None => {
949                    return Err(ExtractorError::InvalidPath);
950                }
951            };
952
953            // Enrichir TOUS les liens
954            let mut all_enriched =
955                enrich_links_with_entities(&state, links, enrichment_context, link_def).await?;
956
957            // Apply filters if provided
958            if let Some(filter_value) = params.filter_value() {
959                all_enriched = apply_link_filters(all_enriched, &filter_value);
960            }
961
962            let total = all_enriched.len();
963
964            // Apply pagination (ALWAYS paginate for nested links too)
965            let page = params.page();
966            let limit = params.limit();
967            let start = (page - 1) * limit;
968
969            let paginated_links: Vec<EnrichedLink> =
970                all_enriched.into_iter().skip(start).take(limit).collect();
971
972            Ok(Json(serde_json::json!({
973                "data": paginated_links,
974                "pagination": {
975                    "page": page,
976                    "limit": limit,
977                    "total": total,
978                    "total_pages": PaginationMeta::new(page, limit, total).total_pages,
979                    "has_next": PaginationMeta::new(page, limit, total).has_next,
980                    "has_prev": PaginationMeta::new(page, limit, total).has_prev
981                },
982                "link_type": link_def.link_type,
983                "direction": format!("{:?}", penultimate.link_direction),
984                "description": link_def.description
985            })))
986        } else {
987            Err(ExtractorError::InvalidPath)
988        }
989    } else {
990        // Item spécifique - récupérer le lien spécifique
991
992        use crate::links::registry::LinkDirection;
993
994        // VALIDATION COMPLÈTE DE LA CHAÎNE (aussi pour items spécifiques)
995        for i in 0..extractor.chain.len() - 1 {
996            let current = &extractor.chain[i];
997            let next = &extractor.chain[i + 1];
998
999            // Cas 1: Le segment a un link_definition → validation normale
1000            if let Some(link_def) = &current.link_definition {
1001                let link_exists = match current.link_direction {
1002                    Some(LinkDirection::Forward) => {
1003                        let links = state
1004                            .link_service
1005                            .find_by_source(
1006                                &current.entity_id,
1007                                Some(&link_def.link_type),
1008                                Some(&link_def.target_type),
1009                            )
1010                            .await
1011                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1012                        links.iter().any(|l| l.target_id == next.entity_id)
1013                    }
1014                    Some(LinkDirection::Reverse) => {
1015                        let links = state
1016                            .link_service
1017                            .find_by_target(&current.entity_id, None, Some(&link_def.link_type))
1018                            .await
1019                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1020                        links.iter().any(|l| l.source_id == next.entity_id)
1021                    }
1022                    None => {
1023                        return Err(ExtractorError::InvalidPath);
1024                    }
1025                };
1026
1027                if !link_exists {
1028                    return Err(ExtractorError::LinkNotFound);
1029                }
1030            }
1031            // Cas 2: Premier segment sans link_definition mais next a un link_definition
1032            else if let Some(next_link_def) = &next.link_definition {
1033                let link_exists = match next.link_direction {
1034                    Some(LinkDirection::Forward) => {
1035                        let links = state
1036                            .link_service
1037                            .find_by_source(
1038                                &current.entity_id,
1039                                Some(&next_link_def.link_type),
1040                                Some(&next_link_def.target_type),
1041                            )
1042                            .await
1043                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1044                        links.iter().any(|l| l.target_id == next.entity_id)
1045                    }
1046                    Some(LinkDirection::Reverse) => {
1047                        let links = state
1048                            .link_service
1049                            .find_by_target(
1050                                &current.entity_id,
1051                                None,
1052                                Some(&next_link_def.link_type),
1053                            )
1054                            .await
1055                            .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1056                        links.iter().any(|l| l.source_id == next.entity_id)
1057                    }
1058                    None => {
1059                        return Err(ExtractorError::InvalidPath);
1060                    }
1061                };
1062
1063                if !link_exists {
1064                    return Err(ExtractorError::LinkNotFound);
1065                }
1066            }
1067        }
1068
1069        // Toute la chaîne est validée, récupérer le lien final
1070        if let Some(link_def) = extractor.final_link_def() {
1071            let (target_id, _) = extractor.final_target();
1072            let penultimate = extractor.penultimate_segment().unwrap();
1073
1074            // Récupérer le lien selon la direction
1075            let link = match penultimate.link_direction {
1076                Some(LinkDirection::Forward) => {
1077                    // Forward: penultimate est la source, target_id est le target
1078                    let links = state
1079                        .link_service
1080                        .find_by_source(
1081                            &penultimate.entity_id,
1082                            Some(&link_def.link_type),
1083                            Some(&link_def.target_type),
1084                        )
1085                        .await
1086                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1087
1088                    links
1089                        .into_iter()
1090                        .find(|l| l.target_id == target_id)
1091                        .ok_or(ExtractorError::LinkNotFound)?
1092                }
1093                Some(LinkDirection::Reverse) => {
1094                    // Reverse: penultimate est le target, target_id est la source
1095                    let links = state
1096                        .link_service
1097                        .find_by_target(&penultimate.entity_id, None, Some(&link_def.link_type))
1098                        .await
1099                        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1100
1101                    links
1102                        .into_iter()
1103                        .find(|l| l.source_id == target_id)
1104                        .ok_or(ExtractorError::LinkNotFound)?
1105                }
1106                None => {
1107                    return Err(ExtractorError::InvalidPath);
1108                }
1109            };
1110
1111            // Enrichir le lien
1112            let enriched = enrich_links_with_entities(
1113                &state,
1114                vec![link],
1115                EnrichmentContext::DirectLink,
1116                link_def,
1117            )
1118            .await?;
1119
1120            Ok(Json(serde_json::json!({
1121                "link": enriched.first()
1122            })))
1123        } else {
1124            Err(ExtractorError::InvalidPath)
1125        }
1126    }
1127}
1128
1129/// Handler générique pour POST sur chemins imbriqués
1130///
1131/// Supporte des chemins comme:
1132/// - POST /users/123/invoices/456/orders (crée un nouvel order + link)
1133pub async fn handle_nested_path_post(
1134    State(state): State<AppState>,
1135    Path(path): Path<String>,
1136    Json(payload): Json<CreateLinkedEntityRequest>,
1137) -> Result<Response, ExtractorError> {
1138    let segments: Vec<String> = path
1139        .trim_matches('/')
1140        .split('/')
1141        .map(|s| s.to_string())
1142        .collect();
1143
1144    // Cette route ne gère QUE les chemins imbriqués à 3+ niveaux (5+ segments)
1145    // Les chemins à 2 niveaux sont gérés par les routes spécifiques
1146    if segments.len() < 5 {
1147        return Err(ExtractorError::InvalidPath);
1148    }
1149
1150    let extractor =
1151        RecursiveLinkExtractor::from_segments(segments, &state.registry, &state.config)?;
1152
1153    // Récupérer le dernier lien
1154    let link_def = extractor
1155        .final_link_def()
1156        .ok_or(ExtractorError::InvalidPath)?;
1157
1158    let (source_id, _) = extractor.final_target();
1159    let target_entity_type = &link_def.target_type;
1160
1161    // Récupérer le creator pour l'entité target
1162    let entity_creator = state
1163        .entity_creators
1164        .get(target_entity_type)
1165        .ok_or_else(|| {
1166            ExtractorError::JsonError(format!(
1167                "No entity creator registered for type: {}",
1168                target_entity_type
1169            ))
1170        })?;
1171
1172    // Créer la nouvelle entité
1173    let created_entity = entity_creator
1174        .create_from_json(payload.entity)
1175        .await
1176        .map_err(|e| ExtractorError::JsonError(format!("Failed to create entity: {}", e)))?;
1177
1178    // Extraire l'ID de l'entité créée
1179    let target_entity_id = created_entity["id"].as_str().ok_or_else(|| {
1180        ExtractorError::JsonError("Created entity missing 'id' field".to_string())
1181    })?;
1182    let target_entity_id = Uuid::parse_str(target_entity_id)
1183        .map_err(|e| ExtractorError::JsonError(format!("Invalid UUID in created entity: {}", e)))?;
1184
1185    // Créer le lien
1186    let link = LinkEntity::new(
1187        link_def.link_type.clone(),
1188        source_id,
1189        target_entity_id,
1190        payload.metadata,
1191    );
1192
1193    let created_link = state
1194        .link_service
1195        .create(link)
1196        .await
1197        .map_err(|e| ExtractorError::JsonError(e.to_string()))?;
1198
1199    // Emit entity created event
1200    state.publish_event(FrameworkEvent::Entity(
1201        crate::core::events::EntityEvent::Created {
1202            entity_type: target_entity_type.clone(),
1203            entity_id: target_entity_id,
1204            data: created_entity.clone(),
1205        },
1206    ));
1207
1208    // Emit link created event
1209    state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1210        link_type: created_link.link_type.clone(),
1211        link_id: created_link.id,
1212        source_id: created_link.source_id,
1213        target_id: created_link.target_id,
1214        metadata: created_link.metadata.clone(),
1215    }));
1216
1217    let response = serde_json::json!({
1218        "entity": created_entity,
1219        "link": created_link,
1220    });
1221
1222    Ok((StatusCode::CREATED, Json(response)).into_response())
1223}
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228    use crate::config::EntityConfig;
1229    use crate::core::LinkDefinition;
1230    use crate::storage::InMemoryLinkService;
1231
1232    fn create_test_state() -> AppState {
1233        let config = Arc::new(LinksConfig {
1234            entities: vec![
1235                EntityConfig {
1236                    singular: "user".to_string(),
1237                    plural: "users".to_string(),
1238                    auth: crate::config::EntityAuthConfig::default(),
1239                },
1240                EntityConfig {
1241                    singular: "car".to_string(),
1242                    plural: "cars".to_string(),
1243                    auth: crate::config::EntityAuthConfig::default(),
1244                },
1245            ],
1246            links: vec![LinkDefinition {
1247                link_type: "owner".to_string(),
1248                source_type: "user".to_string(),
1249                target_type: "car".to_string(),
1250                forward_route_name: "cars-owned".to_string(),
1251                reverse_route_name: "users-owners".to_string(),
1252                description: Some("User owns a car".to_string()),
1253                required_fields: None,
1254                auth: None,
1255            }],
1256            validation_rules: None,
1257            events: None,
1258            sinks: None,
1259        });
1260
1261        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1262        let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1263
1264        AppState {
1265            link_service,
1266            config,
1267            registry,
1268            entity_fetchers: Arc::new(HashMap::new()),
1269            entity_creators: Arc::new(HashMap::new()),
1270            event_bus: None,
1271        }
1272    }
1273
1274    #[test]
1275    fn test_state_creation() {
1276        let state = create_test_state();
1277        assert_eq!(state.config.entities.len(), 2);
1278        assert_eq!(state.config.links.len(), 1);
1279    }
1280
1281    // ======================================================================
1282    // Phase 1: Pure helper function tests
1283    // ======================================================================
1284
1285    // ------------------------------------------------------------------
1286    // get_nested_value
1287    // ------------------------------------------------------------------
1288
1289    #[test]
1290    fn test_get_nested_value_top_level_key() {
1291        let json = serde_json::json!({
1292            "name": "Alice",
1293            "age": 30
1294        });
1295        let result = get_nested_value(&json, "name");
1296        assert_eq!(
1297            result,
1298            Some(serde_json::Value::String("Alice".to_string())),
1299            "should retrieve top-level string value"
1300        );
1301    }
1302
1303    #[test]
1304    fn test_get_nested_value_top_level_number() {
1305        let json = serde_json::json!({ "count": 42 });
1306        let result = get_nested_value(&json, "count");
1307        assert_eq!(
1308            result,
1309            Some(serde_json::json!(42)),
1310            "should retrieve top-level numeric value"
1311        );
1312    }
1313
1314    #[test]
1315    fn test_get_nested_value_missing_top_level_key() {
1316        let json = serde_json::json!({ "name": "Alice" });
1317        let result = get_nested_value(&json, "missing");
1318        assert_eq!(result, None, "missing top-level key should return None");
1319    }
1320
1321    #[test]
1322    fn test_get_nested_value_two_level_path() {
1323        let json = serde_json::json!({
1324            "source": { "name": "Alice", "email": "alice@example.com" },
1325            "target": { "amount": 100 }
1326        });
1327        let result = get_nested_value(&json, "source.name");
1328        assert_eq!(
1329            result,
1330            Some(serde_json::Value::String("Alice".to_string())),
1331            "should navigate two-level dot path"
1332        );
1333    }
1334
1335    #[test]
1336    fn test_get_nested_value_two_level_numeric() {
1337        let json = serde_json::json!({
1338            "target": { "amount": 99.5 }
1339        });
1340        let result = get_nested_value(&json, "target.amount");
1341        assert_eq!(
1342            result,
1343            Some(serde_json::json!(99.5)),
1344            "should retrieve nested numeric value"
1345        );
1346    }
1347
1348    #[test]
1349    fn test_get_nested_value_missing_parent() {
1350        let json = serde_json::json!({ "name": "Alice" });
1351        let result = get_nested_value(&json, "source.name");
1352        assert_eq!(result, None, "missing parent should return None");
1353    }
1354
1355    #[test]
1356    fn test_get_nested_value_missing_child() {
1357        let json = serde_json::json!({ "source": { "name": "Alice" } });
1358        let result = get_nested_value(&json, "source.missing");
1359        assert_eq!(result, None, "missing child key should return None");
1360    }
1361
1362    #[test]
1363    fn test_get_nested_value_three_levels_returns_none() {
1364        let json = serde_json::json!({
1365            "a": { "b": { "c": "deep" } }
1366        });
1367        let result = get_nested_value(&json, "a.b.c");
1368        assert_eq!(
1369            result, None,
1370            "three-level dot path should return None (only 1 or 2 levels supported)"
1371        );
1372    }
1373
1374    #[test]
1375    fn test_get_nested_value_null_value() {
1376        let json = serde_json::json!({ "field": null });
1377        let result = get_nested_value(&json, "field");
1378        assert_eq!(
1379            result,
1380            Some(serde_json::Value::Null),
1381            "null values should be returned as Some(Null)"
1382        );
1383    }
1384
1385    #[test]
1386    fn test_get_nested_value_boolean() {
1387        let json = serde_json::json!({ "active": true });
1388        let result = get_nested_value(&json, "active");
1389        assert_eq!(
1390            result,
1391            Some(serde_json::json!(true)),
1392            "should retrieve boolean value"
1393        );
1394    }
1395
1396    // ------------------------------------------------------------------
1397    // apply_link_filters
1398    // ------------------------------------------------------------------
1399
1400    /// Helper to create an EnrichedLink with configurable fields
1401    fn make_enriched_link(
1402        link_type: &str,
1403        status: &str,
1404        target: Option<serde_json::Value>,
1405        source: Option<serde_json::Value>,
1406        metadata: Option<serde_json::Value>,
1407    ) -> EnrichedLink {
1408        EnrichedLink {
1409            id: Uuid::new_v4(),
1410            entity_type: "link".to_string(),
1411            link_type: link_type.to_string(),
1412            source_id: Uuid::new_v4(),
1413            target_id: Uuid::new_v4(),
1414            source,
1415            target,
1416            metadata,
1417            created_at: chrono::Utc::now(),
1418            updated_at: chrono::Utc::now(),
1419            status: status.to_string(),
1420        }
1421    }
1422
1423    #[test]
1424    fn test_apply_link_filters_null_filter_returns_all() {
1425        let links = vec![
1426            make_enriched_link("owner", "active", None, None, None),
1427            make_enriched_link("driver", "active", None, None, None),
1428        ];
1429        let result = apply_link_filters(links, &serde_json::Value::Null);
1430        assert_eq!(result.len(), 2, "null filter should return all links");
1431    }
1432
1433    #[test]
1434    fn test_apply_link_filters_non_object_filter_returns_all() {
1435        let links = vec![make_enriched_link("owner", "active", None, None, None)];
1436        let result = apply_link_filters(links, &serde_json::json!("not an object"));
1437        assert_eq!(result.len(), 1, "non-object filter should return all links");
1438    }
1439
1440    #[test]
1441    fn test_apply_link_filters_empty_object_returns_all() {
1442        let links = vec![
1443            make_enriched_link("owner", "active", None, None, None),
1444            make_enriched_link("driver", "inactive", None, None, None),
1445        ];
1446        let result = apply_link_filters(links, &serde_json::json!({}));
1447        assert_eq!(
1448            result.len(),
1449            2,
1450            "empty object filter should return all links"
1451        );
1452    }
1453
1454    #[test]
1455    fn test_apply_link_filters_by_status() {
1456        let links = vec![
1457            make_enriched_link("owner", "active", None, None, None),
1458            make_enriched_link("driver", "inactive", None, None, None),
1459            make_enriched_link("owner", "active", None, None, None),
1460        ];
1461        let filter = serde_json::json!({ "status": "active" });
1462        let result = apply_link_filters(links, &filter);
1463        assert_eq!(result.len(), 2, "should filter to only active links");
1464        for link in &result {
1465            assert_eq!(link.status, "active");
1466        }
1467    }
1468
1469    #[test]
1470    fn test_apply_link_filters_by_link_type() {
1471        let links = vec![
1472            make_enriched_link("owner", "active", None, None, None),
1473            make_enriched_link("driver", "active", None, None, None),
1474            make_enriched_link("owner", "active", None, None, None),
1475        ];
1476        let filter = serde_json::json!({ "link_type": "owner" });
1477        let result = apply_link_filters(links, &filter);
1478        assert_eq!(result.len(), 2, "should filter to only 'owner' links");
1479    }
1480
1481    #[test]
1482    fn test_apply_link_filters_by_nested_target_field() {
1483        let links = vec![
1484            make_enriched_link(
1485                "owner",
1486                "active",
1487                Some(serde_json::json!({ "name": "Car A" })),
1488                None,
1489                None,
1490            ),
1491            make_enriched_link(
1492                "owner",
1493                "active",
1494                Some(serde_json::json!({ "name": "Car B" })),
1495                None,
1496                None,
1497            ),
1498        ];
1499        let filter = serde_json::json!({ "target.name": "Car A" });
1500        let result = apply_link_filters(links, &filter);
1501        assert_eq!(result.len(), 1, "should filter by nested target.name");
1502    }
1503
1504    #[test]
1505    fn test_apply_link_filters_by_nested_source_field() {
1506        let links = vec![
1507            make_enriched_link(
1508                "owner",
1509                "active",
1510                None,
1511                Some(serde_json::json!({ "email": "alice@test.com" })),
1512                None,
1513            ),
1514            make_enriched_link(
1515                "owner",
1516                "active",
1517                None,
1518                Some(serde_json::json!({ "email": "bob@test.com" })),
1519                None,
1520            ),
1521        ];
1522        let filter = serde_json::json!({ "source.email": "bob@test.com" });
1523        let result = apply_link_filters(links, &filter);
1524        assert_eq!(result.len(), 1, "should filter by nested source.email");
1525    }
1526
1527    #[test]
1528    fn test_apply_link_filters_multiple_criteria() {
1529        let links = vec![
1530            make_enriched_link("owner", "active", None, None, None),
1531            make_enriched_link("owner", "inactive", None, None, None),
1532            make_enriched_link("driver", "active", None, None, None),
1533        ];
1534        let filter = serde_json::json!({ "link_type": "owner", "status": "active" });
1535        let result = apply_link_filters(links, &filter);
1536        assert_eq!(
1537            result.len(),
1538            1,
1539            "should filter by both link_type AND status"
1540        );
1541        assert_eq!(result[0].link_type, "owner");
1542        assert_eq!(result[0].status, "active");
1543    }
1544
1545    #[test]
1546    fn test_apply_link_filters_no_match_returns_empty() {
1547        let links = vec![make_enriched_link("owner", "active", None, None, None)];
1548        let filter = serde_json::json!({ "status": "deleted" });
1549        let result = apply_link_filters(links, &filter);
1550        assert!(
1551            result.is_empty(),
1552            "non-matching filter should return empty vec"
1553        );
1554    }
1555
1556    #[test]
1557    fn test_apply_link_filters_missing_field_excludes_link() {
1558        let links = vec![make_enriched_link("owner", "active", None, None, None)];
1559        // "nonexistent_field" does not exist on EnrichedLink serialization
1560        let filter = serde_json::json!({ "nonexistent_field": "value" });
1561        let result = apply_link_filters(links, &filter);
1562        assert!(
1563            result.is_empty(),
1564            "filtering by a missing field should exclude the link"
1565        );
1566    }
1567
1568    // ------------------------------------------------------------------
1569    // get_link_auth_policy
1570    // ------------------------------------------------------------------
1571
1572    fn make_link_def_with_auth() -> LinkDefinition {
1573        use crate::core::link::LinkAuthConfig;
1574        LinkDefinition {
1575            link_type: "test".to_string(),
1576            source_type: "a".to_string(),
1577            target_type: "b".to_string(),
1578            forward_route_name: "bs".to_string(),
1579            reverse_route_name: "as".to_string(),
1580            description: None,
1581            required_fields: None,
1582            auth: Some(LinkAuthConfig {
1583                list: "public".to_string(),
1584                get: "authenticated".to_string(),
1585                create: "admin_only".to_string(),
1586                update: "owner".to_string(),
1587                delete: "service_only".to_string(),
1588            }),
1589        }
1590    }
1591
1592    #[test]
1593    fn test_get_link_auth_policy_list() {
1594        let def = make_link_def_with_auth();
1595        let result = AppState::get_link_auth_policy(&def, "list");
1596        assert_eq!(
1597            result,
1598            Some("public".to_string()),
1599            "list operation should return 'public' policy"
1600        );
1601    }
1602
1603    #[test]
1604    fn test_get_link_auth_policy_get() {
1605        let def = make_link_def_with_auth();
1606        let result = AppState::get_link_auth_policy(&def, "get");
1607        assert_eq!(result, Some("authenticated".to_string()));
1608    }
1609
1610    #[test]
1611    fn test_get_link_auth_policy_create() {
1612        let def = make_link_def_with_auth();
1613        let result = AppState::get_link_auth_policy(&def, "create");
1614        assert_eq!(result, Some("admin_only".to_string()));
1615    }
1616
1617    #[test]
1618    fn test_get_link_auth_policy_update() {
1619        let def = make_link_def_with_auth();
1620        let result = AppState::get_link_auth_policy(&def, "update");
1621        assert_eq!(result, Some("owner".to_string()));
1622    }
1623
1624    #[test]
1625    fn test_get_link_auth_policy_delete() {
1626        let def = make_link_def_with_auth();
1627        let result = AppState::get_link_auth_policy(&def, "delete");
1628        assert_eq!(result, Some("service_only".to_string()));
1629    }
1630
1631    #[test]
1632    fn test_get_link_auth_policy_unknown_operation() {
1633        let def = make_link_def_with_auth();
1634        let result = AppState::get_link_auth_policy(&def, "unknown_op");
1635        assert_eq!(
1636            result,
1637            Some("authenticated".to_string()),
1638            "unknown operations should default to 'authenticated'"
1639        );
1640    }
1641
1642    #[test]
1643    fn test_get_link_auth_policy_no_auth_config() {
1644        let def = LinkDefinition {
1645            link_type: "test".to_string(),
1646            source_type: "a".to_string(),
1647            target_type: "b".to_string(),
1648            forward_route_name: "bs".to_string(),
1649            reverse_route_name: "as".to_string(),
1650            description: None,
1651            required_fields: None,
1652            auth: None,
1653        };
1654        let result = AppState::get_link_auth_policy(&def, "list");
1655        assert_eq!(
1656            result, None,
1657            "should return None when no auth config is set"
1658        );
1659    }
1660
1661    // ------------------------------------------------------------------
1662    // publish_event (fire-and-forget, no event bus)
1663    // ------------------------------------------------------------------
1664
1665    #[test]
1666    fn test_publish_event_no_event_bus_does_not_panic() {
1667        let state = create_test_state();
1668        // event_bus is None — should silently drop
1669        state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1670            link_type: "owner".to_string(),
1671            link_id: Uuid::new_v4(),
1672            source_id: Uuid::new_v4(),
1673            target_id: Uuid::new_v4(),
1674            metadata: None,
1675        }));
1676        // If we reach here without panic, the test passes
1677    }
1678
1679    #[test]
1680    fn test_publish_event_with_event_bus() {
1681        let bus = Arc::new(EventBus::new(16));
1682        let mut state = create_test_state();
1683        state.event_bus = Some(bus.clone());
1684
1685        let mut rx = bus.subscribe();
1686
1687        state.publish_event(FrameworkEvent::Link(LinkEvent::Created {
1688            link_type: "owner".to_string(),
1689            link_id: Uuid::new_v4(),
1690            source_id: Uuid::new_v4(),
1691            target_id: Uuid::new_v4(),
1692            metadata: None,
1693        }));
1694
1695        // The event should be receivable
1696        let envelope = rx.try_recv().expect("should receive published event");
1697        assert!(
1698            matches!(
1699                envelope.event,
1700                FrameworkEvent::Link(LinkEvent::Created { .. })
1701            ),
1702            "received event should be a Link::Created"
1703        );
1704    }
1705
1706    // ======================================================================
1707    // Phase 2: Handler tests with InMemoryLinkService
1708    // ======================================================================
1709
1710    /// Extended test config with order -> invoice -> payment chain
1711    fn create_chain_test_state() -> AppState {
1712        let config = Arc::new(LinksConfig {
1713            entities: vec![
1714                EntityConfig {
1715                    singular: "order".to_string(),
1716                    plural: "orders".to_string(),
1717                    auth: crate::config::EntityAuthConfig::default(),
1718                },
1719                EntityConfig {
1720                    singular: "invoice".to_string(),
1721                    plural: "invoices".to_string(),
1722                    auth: crate::config::EntityAuthConfig::default(),
1723                },
1724                EntityConfig {
1725                    singular: "payment".to_string(),
1726                    plural: "payments".to_string(),
1727                    auth: crate::config::EntityAuthConfig::default(),
1728                },
1729            ],
1730            links: vec![
1731                LinkDefinition {
1732                    link_type: "billing".to_string(),
1733                    source_type: "order".to_string(),
1734                    target_type: "invoice".to_string(),
1735                    forward_route_name: "invoices".to_string(),
1736                    reverse_route_name: "order".to_string(),
1737                    description: Some("Order has invoices".to_string()),
1738                    required_fields: None,
1739                    auth: None,
1740                },
1741                LinkDefinition {
1742                    link_type: "payment".to_string(),
1743                    source_type: "invoice".to_string(),
1744                    target_type: "payment".to_string(),
1745                    forward_route_name: "payments".to_string(),
1746                    reverse_route_name: "invoice".to_string(),
1747                    description: None,
1748                    required_fields: None,
1749                    auth: None,
1750                },
1751            ],
1752            validation_rules: None,
1753            events: None,
1754            sinks: None,
1755        });
1756
1757        let registry = Arc::new(LinkRouteRegistry::new(config.clone()));
1758        let link_service: Arc<dyn LinkService> = Arc::new(InMemoryLinkService::new());
1759
1760        AppState {
1761            link_service,
1762            config,
1763            registry,
1764            entity_fetchers: Arc::new(HashMap::new()),
1765            entity_creators: Arc::new(HashMap::new()),
1766            event_bus: None,
1767        }
1768    }
1769
1770    /// Simple EntityFetcher for tests that returns a JSON object with id and name
1771    struct MockEntityFetcher {
1772        entities: std::sync::RwLock<HashMap<Uuid, serde_json::Value>>,
1773    }
1774
1775    impl MockEntityFetcher {
1776        fn new() -> Self {
1777            Self {
1778                entities: std::sync::RwLock::new(HashMap::new()),
1779            }
1780        }
1781
1782        fn insert(&self, id: Uuid, data: serde_json::Value) {
1783            self.entities
1784                .write()
1785                .expect("lock should not be poisoned")
1786                .insert(id, data);
1787        }
1788    }
1789
1790    #[async_trait::async_trait]
1791    impl crate::core::EntityFetcher for MockEntityFetcher {
1792        async fn fetch_as_json(&self, entity_id: &Uuid) -> anyhow::Result<serde_json::Value> {
1793            let entities = self
1794                .entities
1795                .read()
1796                .map_err(|e| anyhow::anyhow!("lock error: {}", e))?;
1797            entities
1798                .get(entity_id)
1799                .cloned()
1800                .ok_or_else(|| anyhow::anyhow!("Entity not found: {}", entity_id))
1801        }
1802    }
1803
1804    /// Simple EntityCreator for tests that returns the input with a generated id
1805    struct MockEntityCreator;
1806
1807    #[async_trait::async_trait]
1808    impl crate::core::EntityCreator for MockEntityCreator {
1809        async fn create_from_json(
1810            &self,
1811            entity_data: serde_json::Value,
1812        ) -> anyhow::Result<serde_json::Value> {
1813            let mut data = entity_data;
1814            if data.get("id").is_none() {
1815                data["id"] = serde_json::json!(Uuid::new_v4().to_string());
1816            }
1817            Ok(data)
1818        }
1819    }
1820
1821    // ------------------------------------------------------------------
1822    // enrich_links_with_entities
1823    // ------------------------------------------------------------------
1824
1825    #[tokio::test]
1826    async fn test_enrich_links_from_source_omits_source() {
1827        let state = create_test_state();
1828        let user_id = Uuid::new_v4();
1829        let car_id = Uuid::new_v4();
1830        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1831
1832        let link_def = &state.config.links[0];
1833        let enriched =
1834            enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1835                .await
1836                .expect("enrichment should succeed");
1837
1838        assert_eq!(enriched.len(), 1);
1839        assert!(
1840            enriched[0].source.is_none(),
1841            "FromSource context should omit source entity"
1842        );
1843        // No fetcher registered, so target will also be None (fetcher not found)
1844        assert!(enriched[0].target.is_none());
1845    }
1846
1847    #[tokio::test]
1848    async fn test_enrich_links_from_target_omits_target() {
1849        let state = create_test_state();
1850        let user_id = Uuid::new_v4();
1851        let car_id = Uuid::new_v4();
1852        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1853
1854        let link_def = &state.config.links[0];
1855        let enriched =
1856            enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromTarget, link_def)
1857                .await
1858                .expect("enrichment should succeed");
1859
1860        assert_eq!(enriched.len(), 1);
1861        assert!(
1862            enriched[0].target.is_none(),
1863            "FromTarget context should omit target entity"
1864        );
1865    }
1866
1867    #[tokio::test]
1868    async fn test_enrich_links_with_fetcher_includes_entity() {
1869        let user_id = Uuid::new_v4();
1870        let car_id = Uuid::new_v4();
1871
1872        let car_fetcher = Arc::new(MockEntityFetcher::new());
1873        car_fetcher.insert(
1874            car_id,
1875            serde_json::json!({ "id": car_id.to_string(), "model": "Tesla" }),
1876        );
1877
1878        let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
1879        fetchers.insert("car".to_string(), car_fetcher);
1880
1881        let mut state = create_test_state();
1882        state.entity_fetchers = Arc::new(fetchers);
1883
1884        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1885        let link_def = &state.config.links[0];
1886
1887        let enriched =
1888            enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1889                .await
1890                .expect("enrichment should succeed");
1891
1892        assert_eq!(enriched.len(), 1);
1893        let target = enriched[0]
1894            .target
1895            .as_ref()
1896            .expect("target entity should be fetched");
1897        assert_eq!(target["model"], "Tesla");
1898    }
1899
1900    #[tokio::test]
1901    async fn test_enrich_links_direct_link_context() {
1902        let user_id = Uuid::new_v4();
1903        let car_id = Uuid::new_v4();
1904
1905        let user_fetcher = Arc::new(MockEntityFetcher::new());
1906        user_fetcher.insert(
1907            user_id,
1908            serde_json::json!({ "id": user_id.to_string(), "name": "Alice" }),
1909        );
1910
1911        let car_fetcher = Arc::new(MockEntityFetcher::new());
1912        car_fetcher.insert(
1913            car_id,
1914            serde_json::json!({ "id": car_id.to_string(), "model": "BMW" }),
1915        );
1916
1917        let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
1918        fetchers.insert("user".to_string(), user_fetcher);
1919        fetchers.insert("car".to_string(), car_fetcher);
1920
1921        let mut state = create_test_state();
1922        state.entity_fetchers = Arc::new(fetchers);
1923
1924        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
1925        let link_def = &state.config.links[0];
1926
1927        let enriched =
1928            enrich_links_with_entities(&state, vec![link], EnrichmentContext::DirectLink, link_def)
1929                .await
1930                .expect("enrichment should succeed");
1931
1932        assert_eq!(enriched.len(), 1);
1933        assert!(
1934            enriched[0].source.is_some(),
1935            "DirectLink context should include source"
1936        );
1937        assert!(
1938            enriched[0].target.is_some(),
1939            "DirectLink context should include target"
1940        );
1941        assert_eq!(
1942            enriched[0].source.as_ref().expect("source")["name"],
1943            "Alice"
1944        );
1945        assert_eq!(enriched[0].target.as_ref().expect("target")["model"], "BMW");
1946    }
1947
1948    #[tokio::test]
1949    async fn test_enrich_links_preserves_metadata() {
1950        let state = create_test_state();
1951        let metadata = serde_json::json!({ "role": "primary" });
1952        let link = crate::core::link::LinkEntity::new(
1953            "owner",
1954            Uuid::new_v4(),
1955            Uuid::new_v4(),
1956            Some(metadata.clone()),
1957        );
1958
1959        let link_def = &state.config.links[0];
1960        let enriched =
1961            enrich_links_with_entities(&state, vec![link], EnrichmentContext::FromSource, link_def)
1962                .await
1963                .expect("enrichment should succeed");
1964
1965        assert_eq!(enriched[0].metadata, Some(metadata));
1966    }
1967
1968    #[tokio::test]
1969    async fn test_enrich_links_empty_input() {
1970        let state = create_test_state();
1971        let link_def = &state.config.links[0];
1972        let enriched =
1973            enrich_links_with_entities(&state, vec![], EnrichmentContext::FromSource, link_def)
1974                .await
1975                .expect("enrichment should succeed");
1976        assert!(
1977            enriched.is_empty(),
1978            "enriching empty vec should return empty vec"
1979        );
1980    }
1981
1982    // ------------------------------------------------------------------
1983    // fetch_entity_by_type
1984    // ------------------------------------------------------------------
1985
1986    #[tokio::test]
1987    async fn test_fetch_entity_by_type_no_fetcher_registered() {
1988        let state = create_test_state();
1989        let result = fetch_entity_by_type(&state, "unknown_type", &Uuid::new_v4()).await;
1990        assert!(
1991            result.is_err(),
1992            "should error when no fetcher is registered"
1993        );
1994        let err_msg = result.unwrap_err().to_string();
1995        assert!(
1996            err_msg.contains("No entity fetcher registered"),
1997            "error should mention missing fetcher, got: {}",
1998            err_msg
1999        );
2000    }
2001
2002    #[tokio::test]
2003    async fn test_fetch_entity_by_type_entity_not_found() {
2004        let fetcher = Arc::new(MockEntityFetcher::new());
2005        let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
2006        fetchers.insert("car".to_string(), fetcher);
2007
2008        let mut state = create_test_state();
2009        state.entity_fetchers = Arc::new(fetchers);
2010
2011        let result = fetch_entity_by_type(&state, "car", &Uuid::new_v4()).await;
2012        assert!(result.is_err(), "should error when entity is not found");
2013    }
2014
2015    #[tokio::test]
2016    async fn test_fetch_entity_by_type_success() {
2017        let car_id = Uuid::new_v4();
2018        let fetcher = Arc::new(MockEntityFetcher::new());
2019        fetcher.insert(
2020            car_id,
2021            serde_json::json!({ "id": car_id.to_string(), "model": "Audi" }),
2022        );
2023
2024        let mut fetchers: HashMap<String, Arc<dyn crate::core::EntityFetcher>> = HashMap::new();
2025        fetchers.insert("car".to_string(), fetcher);
2026
2027        let mut state = create_test_state();
2028        state.entity_fetchers = Arc::new(fetchers);
2029
2030        let result = fetch_entity_by_type(&state, "car", &car_id)
2031            .await
2032            .expect("should succeed");
2033        assert_eq!(result["model"], "Audi");
2034    }
2035
2036    // ------------------------------------------------------------------
2037    // Handler: list_links
2038    // ------------------------------------------------------------------
2039
2040    #[tokio::test]
2041    async fn test_list_links_forward_empty() {
2042        let state = create_test_state();
2043        let user_id = Uuid::new_v4();
2044
2045        let result = list_links(
2046            State(state),
2047            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2048            Query(crate::core::query::QueryParams::default()),
2049        )
2050        .await
2051        .expect("handler should succeed");
2052
2053        let resp = result.0;
2054        assert_eq!(resp.data.len(), 0);
2055        assert_eq!(resp.pagination.total, 0);
2056        assert_eq!(resp.link_type, "owner");
2057        assert_eq!(resp.direction, "Forward");
2058    }
2059
2060    #[tokio::test]
2061    async fn test_list_links_forward_with_links() {
2062        let state = create_test_state();
2063        let user_id = Uuid::new_v4();
2064        let car1_id = Uuid::new_v4();
2065        let car2_id = Uuid::new_v4();
2066
2067        // Create two links
2068        let link1 = crate::core::link::LinkEntity::new("owner", user_id, car1_id, None);
2069        let link2 = crate::core::link::LinkEntity::new("owner", user_id, car2_id, None);
2070        state
2071            .link_service
2072            .create(link1)
2073            .await
2074            .expect("create should succeed");
2075        state
2076            .link_service
2077            .create(link2)
2078            .await
2079            .expect("create should succeed");
2080
2081        let result = list_links(
2082            State(state),
2083            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2084            Query(crate::core::query::QueryParams::default()),
2085        )
2086        .await
2087        .expect("handler should succeed");
2088
2089        let resp = result.0;
2090        assert_eq!(resp.data.len(), 2);
2091        assert_eq!(resp.pagination.total, 2);
2092    }
2093
2094    #[tokio::test]
2095    async fn test_list_links_reverse() {
2096        let state = create_test_state();
2097        let user_id = Uuid::new_v4();
2098        let car_id = Uuid::new_v4();
2099
2100        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2101        state
2102            .link_service
2103            .create(link)
2104            .await
2105            .expect("create should succeed");
2106
2107        let result = list_links(
2108            State(state),
2109            Path(("cars".to_string(), car_id, "users-owners".to_string())),
2110            Query(crate::core::query::QueryParams::default()),
2111        )
2112        .await
2113        .expect("handler should succeed");
2114
2115        let resp = result.0;
2116        assert_eq!(resp.data.len(), 1);
2117        assert_eq!(resp.direction, "Reverse");
2118    }
2119
2120    #[tokio::test]
2121    async fn test_list_links_invalid_route() {
2122        let state = create_test_state();
2123        let result = list_links(
2124            State(state),
2125            Path((
2126                "users".to_string(),
2127                Uuid::new_v4(),
2128                "nonexistent".to_string(),
2129            )),
2130            Query(crate::core::query::QueryParams::default()),
2131        )
2132        .await;
2133
2134        assert!(result.is_err(), "should fail with invalid route");
2135    }
2136
2137    #[tokio::test]
2138    async fn test_list_links_pagination() {
2139        let state = create_test_state();
2140        let user_id = Uuid::new_v4();
2141
2142        // Create 5 links
2143        for _ in 0..5 {
2144            let car_id = Uuid::new_v4();
2145            let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2146            state
2147                .link_service
2148                .create(link)
2149                .await
2150                .expect("create should succeed");
2151        }
2152
2153        let params = crate::core::query::QueryParams {
2154            page: 1,
2155            limit: 2,
2156            filter: None,
2157            sort: None,
2158        };
2159
2160        let result = list_links(
2161            State(state),
2162            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2163            Query(params),
2164        )
2165        .await
2166        .expect("handler should succeed");
2167
2168        let resp = result.0;
2169        assert_eq!(resp.data.len(), 2, "page 1 should have 2 items");
2170        assert_eq!(resp.pagination.total, 5);
2171        assert_eq!(resp.pagination.total_pages, 3);
2172        assert!(resp.pagination.has_next);
2173        assert!(!resp.pagination.has_prev);
2174    }
2175
2176    #[tokio::test]
2177    async fn test_list_links_with_filter() {
2178        let state = create_test_state();
2179        let user_id = Uuid::new_v4();
2180
2181        // Create links: two "owner" links from the same user
2182        let car1_id = Uuid::new_v4();
2183        let car2_id = Uuid::new_v4();
2184        let link1 = crate::core::link::LinkEntity::new("owner", user_id, car1_id, None);
2185        let mut link2 = crate::core::link::LinkEntity::new("owner", user_id, car2_id, None);
2186        link2.status = "inactive".to_string();
2187
2188        state
2189            .link_service
2190            .create(link1)
2191            .await
2192            .expect("create should succeed");
2193        state
2194            .link_service
2195            .create(link2)
2196            .await
2197            .expect("create should succeed");
2198
2199        let params = crate::core::query::QueryParams {
2200            page: 1,
2201            limit: 20,
2202            filter: Some(r#"{"status": "active"}"#.to_string()),
2203            sort: None,
2204        };
2205
2206        let result = list_links(
2207            State(state),
2208            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2209            Query(params),
2210        )
2211        .await
2212        .expect("handler should succeed");
2213
2214        let resp = result.0;
2215        assert_eq!(resp.data.len(), 1, "filter should return only active links");
2216        assert_eq!(resp.data[0].status, "active");
2217    }
2218
2219    // ------------------------------------------------------------------
2220    // Handler: get_link
2221    // ------------------------------------------------------------------
2222
2223    #[tokio::test]
2224    async fn test_get_link_not_found() {
2225        let state = create_test_state();
2226        let result = get_link(State(state), Path(Uuid::new_v4())).await;
2227        assert!(result.is_err(), "should fail for nonexistent link");
2228    }
2229
2230    #[tokio::test]
2231    async fn test_get_link_success() {
2232        let state = create_test_state();
2233        let user_id = Uuid::new_v4();
2234        let car_id = Uuid::new_v4();
2235        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2236        let link_id = link.id;
2237
2238        state
2239            .link_service
2240            .create(link)
2241            .await
2242            .expect("create should succeed");
2243
2244        let result = get_link(State(state), Path(link_id)).await;
2245        assert!(result.is_ok(), "should succeed for existing link");
2246    }
2247
2248    // ------------------------------------------------------------------
2249    // Handler: create_link
2250    // ------------------------------------------------------------------
2251
2252    #[tokio::test]
2253    async fn test_create_link_success() {
2254        let state = create_test_state();
2255        let user_id = Uuid::new_v4();
2256        let car_id = Uuid::new_v4();
2257
2258        let result = create_link(
2259            State(state.clone()),
2260            Path((
2261                "users".to_string(),
2262                user_id,
2263                "cars-owned".to_string(),
2264                car_id,
2265            )),
2266            Json(CreateLinkRequest { metadata: None }),
2267        )
2268        .await;
2269
2270        assert!(result.is_ok(), "create_link should succeed");
2271        let response = result.expect("should be ok");
2272        assert_eq!(response.status(), StatusCode::CREATED);
2273
2274        // Verify the link exists in the service
2275        let links = state
2276            .link_service
2277            .find_by_source(&user_id, Some("owner"), None)
2278            .await
2279            .expect("find_by_source should succeed");
2280        assert_eq!(links.len(), 1);
2281        assert_eq!(links[0].source_id, user_id);
2282        assert_eq!(links[0].target_id, car_id);
2283    }
2284
2285    #[tokio::test]
2286    async fn test_create_link_with_metadata() {
2287        let state = create_test_state();
2288        let user_id = Uuid::new_v4();
2289        let car_id = Uuid::new_v4();
2290
2291        let metadata = serde_json::json!({ "primary_owner": true });
2292
2293        let result = create_link(
2294            State(state.clone()),
2295            Path((
2296                "users".to_string(),
2297                user_id,
2298                "cars-owned".to_string(),
2299                car_id,
2300            )),
2301            Json(CreateLinkRequest {
2302                metadata: Some(metadata.clone()),
2303            }),
2304        )
2305        .await;
2306
2307        assert!(result.is_ok());
2308
2309        let links = state
2310            .link_service
2311            .find_by_source(&user_id, Some("owner"), None)
2312            .await
2313            .expect("find_by_source should succeed");
2314        assert_eq!(links[0].metadata, Some(metadata));
2315    }
2316
2317    #[tokio::test]
2318    async fn test_create_link_invalid_route() {
2319        let state = create_test_state();
2320        let result = create_link(
2321            State(state),
2322            Path((
2323                "users".to_string(),
2324                Uuid::new_v4(),
2325                "nonexistent".to_string(),
2326                Uuid::new_v4(),
2327            )),
2328            Json(CreateLinkRequest { metadata: None }),
2329        )
2330        .await;
2331
2332        assert!(result.is_err(), "should fail with invalid route");
2333    }
2334
2335    // ------------------------------------------------------------------
2336    // Handler: delete_link
2337    // ------------------------------------------------------------------
2338
2339    #[tokio::test]
2340    async fn test_delete_link_success() {
2341        let state = create_test_state();
2342        let user_id = Uuid::new_v4();
2343        let car_id = Uuid::new_v4();
2344
2345        // First create a link
2346        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2347        state
2348            .link_service
2349            .create(link)
2350            .await
2351            .expect("create should succeed");
2352
2353        // Delete it
2354        let result = delete_link(
2355            State(state.clone()),
2356            Path((
2357                "users".to_string(),
2358                user_id,
2359                "cars-owned".to_string(),
2360                car_id,
2361            )),
2362        )
2363        .await;
2364
2365        assert!(result.is_ok(), "delete_link should succeed");
2366        let response = result.expect("should be ok");
2367        assert_eq!(response.status(), StatusCode::NO_CONTENT);
2368
2369        // Verify link is gone
2370        let links = state
2371            .link_service
2372            .find_by_source(&user_id, Some("owner"), None)
2373            .await
2374            .expect("find_by_source should succeed");
2375        assert!(links.is_empty(), "link should be deleted");
2376    }
2377
2378    #[tokio::test]
2379    async fn test_delete_link_not_found() {
2380        let state = create_test_state();
2381        let result = delete_link(
2382            State(state),
2383            Path((
2384                "users".to_string(),
2385                Uuid::new_v4(),
2386                "cars-owned".to_string(),
2387                Uuid::new_v4(),
2388            )),
2389        )
2390        .await;
2391
2392        assert!(result.is_err(), "should fail when link does not exist");
2393    }
2394
2395    // ------------------------------------------------------------------
2396    // Handler: create_linked_entity
2397    // ------------------------------------------------------------------
2398
2399    #[tokio::test]
2400    async fn test_create_linked_entity_success() {
2401        let mut state = create_test_state();
2402        let user_id = Uuid::new_v4();
2403
2404        // Register a mock creator for "car"
2405        let mut creators: HashMap<String, Arc<dyn crate::core::EntityCreator>> = HashMap::new();
2406        creators.insert("car".to_string(), Arc::new(MockEntityCreator));
2407        state.entity_creators = Arc::new(creators);
2408
2409        let entity_data = serde_json::json!({ "model": "Tesla Model 3", "year": 2024 });
2410
2411        let result = create_linked_entity(
2412            State(state.clone()),
2413            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2414            Json(CreateLinkedEntityRequest {
2415                entity: entity_data,
2416                metadata: None,
2417            }),
2418        )
2419        .await;
2420
2421        assert!(result.is_ok(), "create_linked_entity should succeed");
2422        let response = result.expect("should be ok");
2423        assert_eq!(response.status(), StatusCode::CREATED);
2424
2425        // Verify link was created
2426        let links = state
2427            .link_service
2428            .find_by_source(&user_id, Some("owner"), None)
2429            .await
2430            .expect("find_by_source should succeed");
2431        assert_eq!(links.len(), 1, "a link should have been created");
2432    }
2433
2434    #[tokio::test]
2435    async fn test_create_linked_entity_no_creator_registered() {
2436        let state = create_test_state();
2437        let user_id = Uuid::new_v4();
2438
2439        let result = create_linked_entity(
2440            State(state),
2441            Path(("users".to_string(), user_id, "cars-owned".to_string())),
2442            Json(CreateLinkedEntityRequest {
2443                entity: serde_json::json!({}),
2444                metadata: None,
2445            }),
2446        )
2447        .await;
2448
2449        assert!(
2450            result.is_err(),
2451            "should fail when no entity creator is registered"
2452        );
2453    }
2454
2455    // ------------------------------------------------------------------
2456    // Handler: update_link
2457    // ------------------------------------------------------------------
2458
2459    #[tokio::test]
2460    async fn test_update_link_success() {
2461        let state = create_test_state();
2462        let user_id = Uuid::new_v4();
2463        let car_id = Uuid::new_v4();
2464
2465        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2466        state
2467            .link_service
2468            .create(link)
2469            .await
2470            .expect("create should succeed");
2471
2472        let new_metadata = serde_json::json!({ "insured": true });
2473
2474        let result = update_link(
2475            State(state.clone()),
2476            Path((
2477                "users".to_string(),
2478                user_id,
2479                "cars-owned".to_string(),
2480                car_id,
2481            )),
2482            Json(CreateLinkRequest {
2483                metadata: Some(new_metadata.clone()),
2484            }),
2485        )
2486        .await;
2487
2488        assert!(result.is_ok(), "update_link should succeed");
2489
2490        // Verify metadata updated
2491        let links = state
2492            .link_service
2493            .find_by_source(&user_id, Some("owner"), None)
2494            .await
2495            .expect("find_by_source should succeed");
2496        assert_eq!(links[0].metadata, Some(new_metadata));
2497    }
2498
2499    #[tokio::test]
2500    async fn test_update_link_not_found() {
2501        let state = create_test_state();
2502        let result = update_link(
2503            State(state),
2504            Path((
2505                "users".to_string(),
2506                Uuid::new_v4(),
2507                "cars-owned".to_string(),
2508                Uuid::new_v4(),
2509            )),
2510            Json(CreateLinkRequest { metadata: None }),
2511        )
2512        .await;
2513
2514        assert!(result.is_err(), "should fail when link does not exist");
2515    }
2516
2517    // ------------------------------------------------------------------
2518    // Handler: get_link_by_route
2519    // ------------------------------------------------------------------
2520
2521    #[tokio::test]
2522    async fn test_get_link_by_route_forward_success() {
2523        let state = create_test_state();
2524        let user_id = Uuid::new_v4();
2525        let car_id = Uuid::new_v4();
2526
2527        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2528        state
2529            .link_service
2530            .create(link)
2531            .await
2532            .expect("create should succeed");
2533
2534        let result = get_link_by_route(
2535            State(state),
2536            Path((
2537                "users".to_string(),
2538                user_id,
2539                "cars-owned".to_string(),
2540                car_id,
2541            )),
2542        )
2543        .await;
2544
2545        assert!(result.is_ok(), "get_link_by_route should succeed");
2546    }
2547
2548    #[tokio::test]
2549    async fn test_get_link_by_route_not_found() {
2550        let state = create_test_state();
2551        let result = get_link_by_route(
2552            State(state),
2553            Path((
2554                "users".to_string(),
2555                Uuid::new_v4(),
2556                "cars-owned".to_string(),
2557                Uuid::new_v4(),
2558            )),
2559        )
2560        .await;
2561
2562        assert!(result.is_err(), "should fail when link does not exist");
2563    }
2564
2565    // ------------------------------------------------------------------
2566    // Handler: list_available_links
2567    // ------------------------------------------------------------------
2568
2569    #[tokio::test]
2570    async fn test_list_available_links_known_entity() {
2571        let state = create_test_state();
2572        let user_id = Uuid::new_v4();
2573
2574        let result = list_available_links(State(state), Path(("users".to_string(), user_id)))
2575            .await
2576            .expect("handler should succeed");
2577
2578        let resp = result.0;
2579        assert_eq!(resp.entity_type, "user");
2580        assert_eq!(resp.entity_id, user_id);
2581        // user has "cars-owned" forward route
2582        assert!(
2583            !resp.available_routes.is_empty(),
2584            "user should have available routes"
2585        );
2586    }
2587
2588    #[tokio::test]
2589    async fn test_list_available_links_car_has_reverse_routes() {
2590        let state = create_test_state();
2591        let car_id = Uuid::new_v4();
2592
2593        let result = list_available_links(State(state), Path(("cars".to_string(), car_id)))
2594            .await
2595            .expect("handler should succeed");
2596
2597        let resp = result.0;
2598        assert_eq!(resp.entity_type, "car");
2599        assert!(
2600            !resp.available_routes.is_empty(),
2601            "car should have reverse routes"
2602        );
2603        // Should contain "users-owners" reverse route
2604        let route_names: Vec<&str> = resp
2605            .available_routes
2606            .iter()
2607            .map(|r| r.path.as_str())
2608            .collect();
2609        let has_owners = route_names.iter().any(|p| p.contains("users-owners"));
2610        assert!(has_owners, "car should have users-owners route");
2611    }
2612
2613    // ======================================================================
2614    // Phase 3: Nested path handler tests
2615    // ======================================================================
2616
2617    #[tokio::test]
2618    async fn test_handle_nested_path_get_too_few_segments() {
2619        let state = create_chain_test_state();
2620        // Only 3 segments: orders/{id}/invoices — less than 5 segments
2621        let result = handle_nested_path_get(
2622            State(state),
2623            Path("orders/abc/invoices".to_string()),
2624            Query(crate::core::query::QueryParams::default()),
2625        )
2626        .await;
2627
2628        assert!(result.is_err(), "should fail with fewer than 5 segments");
2629    }
2630
2631    #[tokio::test]
2632    async fn test_handle_nested_path_get_list_returns_paginated() {
2633        let state = create_chain_test_state();
2634
2635        let order_id = Uuid::new_v4();
2636        let invoice_id = Uuid::new_v4();
2637        let payment_id = Uuid::new_v4();
2638
2639        // Create the chain: order -> invoice -> payment
2640        let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2641        state
2642            .link_service
2643            .create(link1)
2644            .await
2645            .expect("create should succeed");
2646
2647        let link2 = crate::core::link::LinkEntity::new("payment", invoice_id, payment_id, None);
2648        state
2649            .link_service
2650            .create(link2)
2651            .await
2652            .expect("create should succeed");
2653
2654        // GET /orders/{order_id}/invoices/{invoice_id}/payments (5 segments -> list)
2655        let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2656        let result = handle_nested_path_get(
2657            State(state),
2658            Path(path),
2659            Query(crate::core::query::QueryParams::default()),
2660        )
2661        .await
2662        .expect("handler should succeed");
2663
2664        let json = result.0;
2665        assert!(
2666            json.get("data").is_some(),
2667            "response should contain 'data' field"
2668        );
2669        assert!(
2670            json.get("pagination").is_some(),
2671            "response should contain 'pagination' field"
2672        );
2673        let data = json["data"].as_array().expect("data should be an array");
2674        assert_eq!(data.len(), 1, "should find one payment link");
2675    }
2676
2677    #[tokio::test]
2678    async fn test_handle_nested_path_get_specific_item() {
2679        let state = create_chain_test_state();
2680
2681        let order_id = Uuid::new_v4();
2682        let invoice_id = Uuid::new_v4();
2683        let payment_id = Uuid::new_v4();
2684
2685        let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2686        state
2687            .link_service
2688            .create(link1)
2689            .await
2690            .expect("create should succeed");
2691
2692        let link2 = crate::core::link::LinkEntity::new("payment", invoice_id, payment_id, None);
2693        state
2694            .link_service
2695            .create(link2)
2696            .await
2697            .expect("create should succeed");
2698
2699        // GET /orders/{order_id}/invoices/{invoice_id}/payments/{payment_id} (6 segments -> item)
2700        let path = format!(
2701            "orders/{}/invoices/{}/payments/{}",
2702            order_id, invoice_id, payment_id
2703        );
2704        let result = handle_nested_path_get(
2705            State(state),
2706            Path(path),
2707            Query(crate::core::query::QueryParams::default()),
2708        )
2709        .await
2710        .expect("handler should succeed");
2711
2712        let json = result.0;
2713        assert!(
2714            json.get("link").is_some(),
2715            "response should contain 'link' field for specific item"
2716        );
2717    }
2718
2719    #[tokio::test]
2720    async fn test_handle_nested_path_get_broken_chain() {
2721        let state = create_chain_test_state();
2722
2723        let order_id = Uuid::new_v4();
2724        let invoice_id = Uuid::new_v4();
2725
2726        // Only create order->invoice link, but NOT invoice->payment
2727        let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2728        state
2729            .link_service
2730            .create(link1)
2731            .await
2732            .expect("create should succeed");
2733
2734        // Try to get a specific payment through the chain — chain validation should fail
2735        // because order->invoice exists but invoice->payment doesn't for this specific item
2736        let fake_payment_id = Uuid::new_v4();
2737        let path = format!(
2738            "orders/{}/invoices/{}/payments/{}",
2739            order_id, invoice_id, fake_payment_id
2740        );
2741        let result = handle_nested_path_get(
2742            State(state),
2743            Path(path),
2744            Query(crate::core::query::QueryParams::default()),
2745        )
2746        .await;
2747
2748        assert!(
2749            result.is_err(),
2750            "should fail when link chain is broken (no payment link)"
2751        );
2752    }
2753
2754    #[tokio::test]
2755    async fn test_handle_nested_path_get_invalid_chain_first_link() {
2756        let state = create_chain_test_state();
2757
2758        let order_id = Uuid::new_v4();
2759        let wrong_invoice_id = Uuid::new_v4();
2760        let payment_id = Uuid::new_v4();
2761
2762        // Create link from DIFFERENT order, not from order_id
2763        let other_order_id = Uuid::new_v4();
2764        let link1 =
2765            crate::core::link::LinkEntity::new("billing", other_order_id, wrong_invoice_id, None);
2766        state
2767            .link_service
2768            .create(link1)
2769            .await
2770            .expect("create should succeed");
2771
2772        let link2 =
2773            crate::core::link::LinkEntity::new("payment", wrong_invoice_id, payment_id, None);
2774        state
2775            .link_service
2776            .create(link2)
2777            .await
2778            .expect("create should succeed");
2779
2780        // Try to traverse: orders/{order_id}/invoices/{wrong_invoice_id}/payments
2781        // The first link (order_id -> wrong_invoice_id) does not exist
2782        let path = format!("orders/{}/invoices/{}/payments", order_id, wrong_invoice_id);
2783        let result = handle_nested_path_get(
2784            State(state),
2785            Path(path),
2786            Query(crate::core::query::QueryParams::default()),
2787        )
2788        .await;
2789
2790        assert!(
2791            result.is_err(),
2792            "should fail when first link in chain does not exist"
2793        );
2794    }
2795
2796    // ------------------------------------------------------------------
2797    // Handler: handle_nested_path_post
2798    // ------------------------------------------------------------------
2799
2800    #[tokio::test]
2801    async fn test_handle_nested_path_post_too_few_segments() {
2802        let state = create_chain_test_state();
2803        let result = handle_nested_path_post(
2804            State(state),
2805            Path("orders/abc/invoices".to_string()),
2806            Json(CreateLinkedEntityRequest {
2807                entity: serde_json::json!({}),
2808                metadata: None,
2809            }),
2810        )
2811        .await;
2812
2813        assert!(result.is_err(), "should fail with fewer than 5 segments");
2814    }
2815
2816    #[tokio::test]
2817    async fn test_handle_nested_path_post_success() {
2818        let mut state = create_chain_test_state();
2819
2820        // Register mock creator for payment
2821        let mut creators: HashMap<String, Arc<dyn crate::core::EntityCreator>> = HashMap::new();
2822        creators.insert("payment".to_string(), Arc::new(MockEntityCreator));
2823        state.entity_creators = Arc::new(creators);
2824
2825        let order_id = Uuid::new_v4();
2826        let invoice_id = Uuid::new_v4();
2827
2828        // Create the prerequisite chain
2829        let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2830        state
2831            .link_service
2832            .create(link1)
2833            .await
2834            .expect("create should succeed");
2835
2836        // POST /orders/{order_id}/invoices/{invoice_id}/payments
2837        let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2838        let result = handle_nested_path_post(
2839            State(state.clone()),
2840            Path(path),
2841            Json(CreateLinkedEntityRequest {
2842                entity: serde_json::json!({ "amount": 100.0 }),
2843                metadata: None,
2844            }),
2845        )
2846        .await;
2847
2848        assert!(result.is_ok(), "handle_nested_path_post should succeed");
2849        let response = result.expect("should be ok");
2850        assert_eq!(response.status(), StatusCode::CREATED);
2851    }
2852
2853    #[tokio::test]
2854    async fn test_handle_nested_path_post_no_creator() {
2855        let state = create_chain_test_state();
2856
2857        let order_id = Uuid::new_v4();
2858        let invoice_id = Uuid::new_v4();
2859
2860        let link1 = crate::core::link::LinkEntity::new("billing", order_id, invoice_id, None);
2861        state
2862            .link_service
2863            .create(link1)
2864            .await
2865            .expect("create should succeed");
2866
2867        let path = format!("orders/{}/invoices/{}/payments", order_id, invoice_id);
2868        let result = handle_nested_path_post(
2869            State(state),
2870            Path(path),
2871            Json(CreateLinkedEntityRequest {
2872                entity: serde_json::json!({}),
2873                metadata: None,
2874            }),
2875        )
2876        .await;
2877
2878        assert!(
2879            result.is_err(),
2880            "should fail when no entity creator is registered for target type"
2881        );
2882    }
2883
2884    // ------------------------------------------------------------------
2885    // EnrichedLink serialization
2886    // ------------------------------------------------------------------
2887
2888    #[test]
2889    fn test_enriched_link_skips_none_source_and_target() {
2890        let link = make_enriched_link("owner", "active", None, None, None);
2891        let json = serde_json::to_value(&link).expect("serialization should succeed");
2892        assert!(
2893            json.get("source").is_none(),
2894            "None source should be skipped in serialization"
2895        );
2896        assert!(
2897            json.get("target").is_none(),
2898            "None target should be skipped in serialization"
2899        );
2900        assert!(
2901            json.get("metadata").is_none(),
2902            "None metadata should be skipped in serialization"
2903        );
2904    }
2905
2906    #[test]
2907    fn test_enriched_link_includes_present_fields() {
2908        let link = make_enriched_link(
2909            "owner",
2910            "active",
2911            Some(serde_json::json!({ "name": "Car" })),
2912            Some(serde_json::json!({ "name": "User" })),
2913            Some(serde_json::json!({ "priority": 1 })),
2914        );
2915        let json = serde_json::to_value(&link).expect("serialization should succeed");
2916        assert!(json.get("source").is_some());
2917        assert!(json.get("target").is_some());
2918        assert!(json.get("metadata").is_some());
2919        assert_eq!(json["type"], "link");
2920    }
2921
2922    // ------------------------------------------------------------------
2923    // AppState with event bus integration
2924    // ------------------------------------------------------------------
2925
2926    #[tokio::test]
2927    async fn test_create_link_emits_event() {
2928        let bus = Arc::new(EventBus::new(16));
2929        let mut state = create_test_state();
2930        state.event_bus = Some(bus.clone());
2931
2932        let mut rx = bus.subscribe();
2933
2934        let user_id = Uuid::new_v4();
2935        let car_id = Uuid::new_v4();
2936
2937        let _result = create_link(
2938            State(state),
2939            Path((
2940                "users".to_string(),
2941                user_id,
2942                "cars-owned".to_string(),
2943                car_id,
2944            )),
2945            Json(CreateLinkRequest { metadata: None }),
2946        )
2947        .await
2948        .expect("create should succeed");
2949
2950        let envelope = rx.try_recv().expect("should receive link created event");
2951        match envelope.event {
2952            FrameworkEvent::Link(LinkEvent::Created {
2953                link_type,
2954                source_id,
2955                target_id,
2956                ..
2957            }) => {
2958                assert_eq!(link_type, "owner");
2959                assert_eq!(source_id, user_id);
2960                assert_eq!(target_id, car_id);
2961            }
2962            other => panic!("expected Link::Created event, got: {:?}", other),
2963        }
2964    }
2965
2966    #[tokio::test]
2967    async fn test_delete_link_emits_event() {
2968        let bus = Arc::new(EventBus::new(16));
2969        let mut state = create_test_state();
2970        state.event_bus = Some(bus.clone());
2971
2972        let user_id = Uuid::new_v4();
2973        let car_id = Uuid::new_v4();
2974        let link = crate::core::link::LinkEntity::new("owner", user_id, car_id, None);
2975        state
2976            .link_service
2977            .create(link)
2978            .await
2979            .expect("create should succeed");
2980
2981        let mut rx = bus.subscribe();
2982
2983        delete_link(
2984            State(state),
2985            Path((
2986                "users".to_string(),
2987                user_id,
2988                "cars-owned".to_string(),
2989                car_id,
2990            )),
2991        )
2992        .await
2993        .expect("delete should succeed");
2994
2995        let envelope = rx.try_recv().expect("should receive link deleted event");
2996        match envelope.event {
2997            FrameworkEvent::Link(LinkEvent::Deleted {
2998                link_type,
2999                source_id,
3000                target_id,
3001                ..
3002            }) => {
3003                assert_eq!(link_type, "owner");
3004                assert_eq!(source_id, user_id);
3005                assert_eq!(target_id, car_id);
3006            }
3007            other => panic!("expected Link::Deleted event, got: {:?}", other),
3008        }
3009    }
3010}