use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Serializable per-row sink error returned from pipeline execution routes.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PipelineExecutionError {
    pub index: usize,
    pub error: String,
}

impl PipelineExecutionError {
    /// Builds a pipeline execution error entry for a failed sink row.
    pub fn new(index: usize, error: impl Into<String>) -> Self {
        Self {
            index,
            error: error.into(),
        }
    }
}

/// Serializable success envelope returned from `/pipelines` and `/pipelines/simulate`.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PipelineExecutionResponse {
    pub data: Vec<Value>,
    pub rows_fetched: usize,
    pub rows_inserted: usize,
    pub errors: Vec<PipelineExecutionError>,
    pub would_insert: Option<usize>,
    pub dry_run: bool,
    pub source_client: String,
    pub sink_client: String,
    pub pipeline_run_id: String,
}

impl PipelineExecutionResponse {
    /// Builds the canonical pipeline execution response envelope.
    pub fn new(
        data: Vec<Value>,
        rows_fetched: usize,
        rows_inserted: usize,
        errors: Vec<PipelineExecutionError>,
        dry_run: bool,
        source_client: impl Into<String>,
        sink_client: impl Into<String>,
        pipeline_run_id: impl Into<String>,
    ) -> Self {
        let would_insert = dry_run.then_some(rows_fetched);

        Self {
            data,
            rows_fetched,
            rows_inserted,
            errors,
            would_insert,
            dry_run,
            source_client: source_client.into(),
            sink_client: sink_client.into(),
            pipeline_run_id: pipeline_run_id.into(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn execution_response_serializes_dry_run_shape() {
        let response = PipelineExecutionResponse::new(
            vec![json!({ "workspace_id": "acme" })],
            3,
            0,
            vec![],
            true,
            "source_client",
            "sink_client",
            "run_123",
        );

        assert_eq!(
            serde_json::to_value(&response).expect("serialize response"),
            json!({
                "data": [{ "workspace_id": "acme" }],
                "rows_fetched": 3,
                "rows_inserted": 0,
                "errors": [],
                "would_insert": 3,
                "dry_run": true,
                "source_client": "source_client",
                "sink_client": "sink_client",
                "pipeline_run_id": "run_123"
            })
        );
    }

    #[test]
    fn execution_response_serializes_null_would_insert_for_non_dry_run() {
        let response = PipelineExecutionResponse::new(
            vec![json!({ "workspace_id": "acme" })],
            1,
            1,
            vec![PipelineExecutionError::new(0, "duplicate key")],
            false,
            "source_client",
            "sink_client",
            "run_456",
        );

        assert_eq!(
            serde_json::to_value(&response).expect("serialize response"),
            json!({
                "data": [{ "workspace_id": "acme" }],
                "rows_fetched": 1,
                "rows_inserted": 1,
                "errors": [{ "index": 0, "error": "duplicate key" }],
                "would_insert": null,
                "dry_run": false,
                "source_client": "source_client",
                "sink_client": "sink_client",
                "pipeline_run_id": "run_456"
            })
        );
    }
}
