//! HTTP wizard endpoints — REST API for the project setup wizard.
use crate::http::context::{AppContext, OpenApiResult, bad_request, not_found};
use crate::io::wizard::{WizardState, WizardStep};
use crate::service::wizard as svc;
use poem_openapi::{Object, OpenApi, Tags, param::Path, payload::Json};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Tags)]
enum WizardTags {
    Wizard,
}

/// Response for a single wizard step.
#[derive(Serialize, Object)]
struct StepResponse {
    step: String,
    label: String,
    status: String,
    #[oai(skip_serializing_if = "Option::is_none")]
    content: Option<String>,
}

/// Full wizard state response.
#[derive(Serialize, Object)]
struct WizardResponse {
    steps: Vec<StepResponse>,
    current_step_index: usize,
    completed: bool,
}

/// Request body for confirming/skipping a step or submitting content.
#[derive(Deserialize, Object)]
struct StepActionPayload {
    /// Optional content to store for the step (e.g., generated spec).
    #[oai(skip_serializing_if = "Option::is_none")]
    content: Option<String>,
}

impl From<&WizardState> for WizardResponse {
    fn from(state: &WizardState) -> Self {
        WizardResponse {
            steps: state
                .steps
                .iter()
                .map(|s| StepResponse {
                    step: serde_json::to_value(s.step)
                        .ok()
                        .and_then(|v| v.as_str().map(String::from))
                        .unwrap_or_default(),
                    label: s.step.label().to_string(),
                    status: serde_json::to_value(&s.status)
                        .ok()
                        .and_then(|v| v.as_str().map(String::from))
                        .unwrap_or_default(),
                    content: s.content.clone(),
                })
                .collect(),
            current_step_index: state.current_step_index(),
            completed: state.completed,
        }
    }
}

fn parse_step(step_str: &str) -> Result<WizardStep, poem::Error> {
    let quoted = format!("\"{step_str}\"");
    serde_json::from_str::<WizardStep>(&quoted)
        .map_err(|_| not_found(format!("Unknown wizard step: {step_str}")))
}

pub struct WizardApi {
    pub ctx: Arc<AppContext>,
}

#[OpenApi(tag = "WizardTags::Wizard")]
impl WizardApi {
    /// Get the current wizard state.
    ///
    /// Returns the full setup wizard progress including all steps and their
    /// statuses. Returns 404 if no wizard is active.
    #[oai(path = "/wizard", method = "get")]
    async fn get_wizard_state(&self) -> OpenApiResult<Json<WizardResponse>> {
        let root = self.ctx.state.get_project_root().map_err(bad_request)?;
        let state = svc::get_state(&root).map_err(|_| not_found("No wizard active".to_string()))?;
        Ok(Json(WizardResponse::from(&state)))
    }

    /// Set a step's content and mark it as awaiting confirmation.
    ///
    /// Used after the agent generates content for a step. The content is
    /// stored for preview and the step is marked as awaiting user confirmation.
    #[oai(path = "/wizard/step/:step/content", method = "put")]
    async fn set_step_content(
        &self,
        step: Path<String>,
        payload: Json<StepActionPayload>,
    ) -> OpenApiResult<Json<WizardResponse>> {
        let root = self.ctx.state.get_project_root().map_err(bad_request)?;
        let wizard_step = parse_step(&step.0)?;
        let state = svc::set_step_content(&root, wizard_step, payload.0.content)
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(WizardResponse::from(&state)))
    }

    /// Confirm a step and advance to the next.
    ///
    /// The step must be the current active step. Returns the updated wizard state.
    #[oai(path = "/wizard/step/:step/confirm", method = "post")]
    async fn confirm_step(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
        let root = self.ctx.state.get_project_root().map_err(bad_request)?;
        let wizard_step = parse_step(&step.0)?;
        let state =
            svc::mark_step_confirmed(&root, wizard_step).map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(WizardResponse::from(&state)))
    }

    /// Skip a step and advance to the next.
    ///
    /// The step must be the current active step.
    #[oai(path = "/wizard/step/:step/skip", method = "post")]
    async fn skip_step(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
        let root = self.ctx.state.get_project_root().map_err(bad_request)?;
        let wizard_step = parse_step(&step.0)?;
        let state =
            svc::mark_step_skipped(&root, wizard_step).map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(WizardResponse::from(&state)))
    }

    /// Mark a step as generating (agent is working on it).
    #[oai(path = "/wizard/step/:step/generating", method = "post")]
    async fn mark_generating(&self, step: Path<String>) -> OpenApiResult<Json<WizardResponse>> {
        let root = self.ctx.state.get_project_root().map_err(bad_request)?;
        let wizard_step = parse_step(&step.0)?;
        let state = svc::mark_step_generating(&root, wizard_step)
            .map_err(|e| bad_request(e.to_string()))?;
        Ok(Json(WizardResponse::from(&state)))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::http::context::AppContext;
    use poem::http::StatusCode;
    use poem::test::TestClient;
    use poem_openapi::OpenApiService;
    use tempfile::TempDir;

    fn setup() -> (TempDir, TestClient<impl poem::Endpoint>) {
        let dir = TempDir::new().unwrap();
        let root = dir.path().to_path_buf();
        std::fs::create_dir_all(root.join(".huskies")).unwrap();

        let ctx = Arc::new(AppContext::new_test(root.clone()));
        let api = WizardApi { ctx };
        let service = OpenApiService::new(api, "test", "0.1.0");
        let client = TestClient::new(service);
        (dir, client)
    }

    #[tokio::test]
    async fn get_wizard_returns_404_when_no_wizard() {
        let (_dir, client) = setup();
        let resp = client.get("/wizard").send().await;
        resp.assert_status(StatusCode::NOT_FOUND);
    }

    #[tokio::test]
    async fn get_wizard_returns_state_when_active() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client.get("/wizard").send().await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert_eq!(body["current_step_index"], 1);
        assert!(!body["completed"].as_bool().unwrap());
        assert_eq!(body["steps"].as_array().unwrap().len(), 8);
        assert_eq!(body["steps"][0]["status"], "confirmed");
    }

    #[tokio::test]
    async fn confirm_step_advances_wizard() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client.post("/wizard/step/context/confirm").send().await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert_eq!(body["current_step_index"], 2);
        assert_eq!(body["steps"][1]["status"], "confirmed");
    }

    #[tokio::test]
    async fn confirm_wrong_step_returns_error() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        // Try to confirm step 3 (stack) when current is step 2 (context)
        let resp = client.post("/wizard/step/stack/confirm").send().await;
        resp.assert_status(StatusCode::BAD_REQUEST);
    }

    #[tokio::test]
    async fn skip_step_advances_wizard() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client.post("/wizard/step/context/skip").send().await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert_eq!(body["steps"][1]["status"], "skipped");
        assert_eq!(body["current_step_index"], 2);
    }

    #[tokio::test]
    async fn set_step_content_marks_awaiting_confirmation() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client
            .put("/wizard/step/context/content")
            .body_json(&serde_json::json!({
                "content": "# My Project\n\nA great project."
            }))
            .send()
            .await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert_eq!(body["steps"][1]["status"], "awaiting_confirmation");
        assert_eq!(
            body["steps"][1]["content"],
            "# My Project\n\nA great project."
        );
    }

    #[tokio::test]
    async fn mark_generating_updates_step() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client.post("/wizard/step/context/generating").send().await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert_eq!(body["steps"][1]["status"], "generating");
    }

    #[tokio::test]
    async fn unknown_step_returns_404() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        let resp = client.post("/wizard/step/nonexistent/confirm").send().await;
        resp.assert_status(StatusCode::NOT_FOUND);
    }

    #[tokio::test]
    async fn full_wizard_flow_completes() {
        let (dir, client) = setup();
        WizardState::init_if_missing(dir.path());

        // Steps 2-8 (scaffold is already confirmed)
        let steps = [
            "context",
            "stack",
            "test_script",
            "build_script",
            "lint_script",
            "release_script",
            "test_coverage",
        ];
        for step in steps {
            let resp = client
                .post(format!("/wizard/step/{step}/confirm"))
                .send()
                .await;
            resp.assert_status_is_ok();
        }

        // Check final state
        let resp = client.get("/wizard").send().await;
        resp.assert_status_is_ok();
        let body: serde_json::Value = resp.0.into_body().into_json().await.unwrap();
        assert!(body["completed"].as_bool().unwrap());
    }
}
