//! Cassette-backed OpenRouter long-session and tool-contract regression tests.
//!
//! These scenarios stress OpenRouter's OpenAI-compatible chat-completions path
//! with multi-turn tool loops, streamed tool-call deltas, complex JSON tool
//! arguments, explicit tool choice, long caller-owned chat history, and usage
//! surfaced from routed upstream providers.

use std::sync::{Arc, Mutex};

use anyhow::Result;
use rig::OneOrMany;
use rig::completion::{Chat, CompletionModel, Message, TypedPrompt};
use rig::message::{AssistantContent, ToolChoice, UserContent};
use rig::prelude::*;
use rig::streaming::{StreamingChat, StreamingPrompt};
use rig::tool::Tool;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::json;

use crate::support::{
    ALPHA_SIGNAL_OUTPUT, AlphaSignal, BETA_SIGNAL_OUTPUT, BetaSignal, TWO_TOOL_STREAM_PREAMBLE,
    TWO_TOOL_STREAM_PROMPT, assert_contains_all_case_insensitive, assert_nonempty_response,
    assert_raw_stream_tool_call_arguments_are_objects, assert_two_tool_roundtrip_contract,
    collect_raw_stream_observation, collect_stream_observation,
};

use super::super::{TOOL_MODEL, support::with_openrouter_cassette_result};

const SESSION_MODEL: &str = TOOL_MODEL;
const STRUCTURED_MODEL: &str = "google/gemini-2.5-flash";

const COMPLEX_SESSION_PREAMBLE: &str = "\
You are a deterministic OpenRouter tool orchestration test harness. Use the tools instead of inventing values. \
For the production-readiness scenario, call exactly one tool at a time in this order: \
1. ping_empty with an empty JSON object. \
2. inspect_manifest with project rig-openrouter, flags critical=true and retries=2, steps plan weight=1 and verify weight=2, and the exact note from the user. \
3. join_labels with labels [north, beta gamma, quote:\"delta\", slash\\path] and separator |. \
4. escape_echo with the exact escaped text from the user. \
After all tool results are available, answer in one short sentence that includes EMPTY-OK, MANIFEST-OK, LABELS-OK, and ESCAPE-OK.";

const COMPLEX_SESSION_PROMPT: &str = "\
Run the production-readiness scenario. The manifest note is `line one; line two says \"hello\" and path C:\\rig\\openrouter`. \
The escaped text is `Line 1\nLine \"2\" with backslash \\ and unicode snowman ☃`.";

#[derive(Clone, Debug, PartialEq)]
struct ToolInvocation {
    name: &'static str,
    args: serde_json::Value,
}

type InvocationLog = Arc<Mutex<Vec<ToolInvocation>>>;

fn push_invocation<T: Serialize>(log: &InvocationLog, name: &'static str, args: &T) {
    log.lock()
        .expect("tool invocation log lock should not be poisoned")
        .push(ToolInvocation {
            name,
            args: serde_json::to_value(args).expect("tool args should serialize"),
        });
}

#[derive(Clone)]
struct PingEmpty {
    log: InvocationLog,
}

#[derive(Clone)]
struct InspectManifest {
    log: InvocationLog,
}

#[derive(Clone)]
struct JoinLabels {
    log: InvocationLog,
}

#[derive(Clone)]
struct EscapeEcho {
    log: InvocationLog,
}

#[derive(Debug, Deserialize, Serialize)]
struct EmptyArgs {}

#[derive(Debug, Deserialize, Serialize)]
struct ManifestArgs {
    project: String,
    flags: ManifestFlags,
    steps: Vec<ManifestStep>,
    note: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct ManifestFlags {
    critical: bool,
    retries: u8,
}

#[derive(Debug, Deserialize, Serialize)]
struct ManifestStep {
    name: String,
    weight: i32,
}

#[derive(Debug, Deserialize, Serialize)]
struct JoinArgs {
    labels: Vec<String>,
    separator: String,
}

#[derive(Debug, Deserialize, Serialize)]
struct EchoArgs {
    text: String,
}

#[derive(Debug, thiserror::Error)]
#[error("session tool error")]
struct SessionToolError;

impl Tool for PingEmpty {
    const NAME: &'static str = "ping_empty";
    type Error = SessionToolError;
    type Args = EmptyArgs;
    type Output = String;

    fn description(&self) -> String {
        "Return EMPTY-OK. This tool takes no arguments.".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {},
            "required": []
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        push_invocation(&self.log, Self::NAME, &args);
        Ok("EMPTY-OK".to_string())
    }
}

impl Tool for InspectManifest {
    const NAME: &'static str = "inspect_manifest";
    type Error = SessionToolError;
    type Args = ManifestArgs;
    type Output = String;

    fn description(&self) -> String {
        "Validate a nested deployment manifest.".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "project": { "type": "string" },
                "flags": {
                    "type": "object",
                    "properties": {
                        "critical": { "type": "boolean" },
                        "retries": { "type": "integer" }
                    },
                    "required": ["critical", "retries"]
                },
                "steps": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": { "type": "string" },
                            "weight": { "type": "integer" }
                        },
                        "required": ["name", "weight"]
                    }
                },
                "note": { "type": "string" }
            },
            "required": ["project", "flags", "steps", "note"]
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        push_invocation(&self.log, Self::NAME, &args);
        Ok(format!(
            "MANIFEST-OK project={} steps={} retries={}",
            args.project,
            args.steps.len(),
            args.flags.retries
        ))
    }
}

impl Tool for JoinLabels {
    const NAME: &'static str = "join_labels";
    type Error = SessionToolError;
    type Args = JoinArgs;
    type Output = String;

    fn description(&self) -> String {
        "Join label strings with the requested separator.".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "labels": {
                    "type": "array",
                    "items": { "type": "string" }
                },
                "separator": { "type": "string" }
            },
            "required": ["labels", "separator"]
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        push_invocation(&self.log, Self::NAME, &args);
        Ok(format!("LABELS-OK {}", args.labels.join(&args.separator)))
    }
}

impl Tool for EscapeEcho {
    const NAME: &'static str = "escape_echo";
    type Error = SessionToolError;
    type Args = EchoArgs;
    type Output = String;

    fn description(&self) -> String {
        "Echo a string containing escaping-sensitive characters.".to_string()
    }

    fn parameters(&self) -> serde_json::Value {
        json!({
            "type": "object",
            "properties": {
                "text": { "type": "string" }
            },
            "required": ["text"]
        })
    }

    async fn call(
        &self,
        _context: &mut rig::tool::ToolContext,
        args: Self::Args,
    ) -> Result<Self::Output, Self::Error> {
        push_invocation(&self.log, Self::NAME, &args);
        Ok(format!("ESCAPE-OK {}", args.text))
    }
}

fn complex_tools(log: &InvocationLog) -> (PingEmpty, InspectManifest, JoinLabels, EscapeEcho) {
    (
        PingEmpty { log: log.clone() },
        InspectManifest { log: log.clone() },
        JoinLabels { log: log.clone() },
        EscapeEcho { log: log.clone() },
    )
}

fn assert_complex_invocations(log: &InvocationLog) {
    let invocations = log
        .lock()
        .expect("tool invocation log lock should not be poisoned")
        .clone();
    let names = invocations.iter().map(|call| call.name).collect::<Vec<_>>();
    assert_eq!(
        names,
        vec![
            PingEmpty::NAME,
            InspectManifest::NAME,
            JoinLabels::NAME,
            EscapeEcho::NAME,
        ],
        "expected one complex tool call of each shape in order"
    );

    assert_eq!(invocations[0].args, json!({}));
    assert_eq!(invocations[1].args["project"], "rig-openrouter");
    assert_eq!(
        invocations[1].args["flags"],
        json!({"critical": true, "retries": 2})
    );
    assert_eq!(
        invocations[1].args["steps"].as_array().map(Vec::len),
        Some(2)
    );
    assert_eq!(
        invocations[1].args["note"],
        "line one; line two says \"hello\" and path C:\\rig\\openrouter"
    );
    assert_eq!(
        invocations[2].args,
        json!({
            "labels": ["north", "beta gamma", "quote:\"delta\"", "slash\\path"],
            "separator": "|"
        })
    );
    assert_eq!(
        invocations[3].args["text"],
        "Line 1\nLine \"2\" with backslash \\ and unicode snowman ☃"
    );
}

struct ToolEvent {
    message_index: usize,
    name_or_id: String,
}

fn history_tool_calls(history: &[Message]) -> Vec<ToolEvent> {
    let mut calls = Vec::new();
    for (message_index, message) in history.iter().enumerate() {
        if let Message::Assistant { content, .. } = message {
            for item in content.iter() {
                if let AssistantContent::ToolCall(tool_call) = item {
                    calls.push(ToolEvent {
                        message_index,
                        name_or_id: tool_call.function.name.clone(),
                    });
                }
            }
        }
    }
    calls
}

fn history_tool_results(history: &[Message]) -> Vec<ToolEvent> {
    let mut results = Vec::new();
    for (message_index, message) in history.iter().enumerate() {
        if let Message::User { content } = message {
            for item in content.iter() {
                if let UserContent::ToolResult(tool_result) = item {
                    results.push(ToolEvent {
                        message_index,
                        name_or_id: tool_result.id.clone(),
                    });
                }
            }
        }
    }
    results
}

fn assert_history_records_sequential_tool_roundtrips(history: &[Message], expected_tools: &[&str]) {
    let calls = history_tool_calls(history);
    let results = history_tool_results(history);

    assert_eq!(
        calls
            .iter()
            .map(|call| call.name_or_id.as_str())
            .collect::<Vec<_>>(),
        expected_tools,
        "caller-owned chat history should preserve tool call order"
    );
    assert_eq!(
        results.len(),
        expected_tools.len(),
        "caller-owned chat history should contain one tool result per call"
    );

    for (index, call) in calls.iter().enumerate() {
        let result = &results[index];
        assert!(
            call.message_index < result.message_index,
            "tool result should follow its assistant tool call"
        );
        if let Some(next_call) = calls.get(index + 1) {
            assert!(
                result.message_index < next_call.message_index,
                "next tool call should occur after the previous tool result"
            );
        }
    }
}

#[tokio::test]
async fn sequential_complex_tool_calls_nonstreaming() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/sequential_complex_tool_calls_nonstreaming",
        |client| async move {
            let log = Arc::new(Mutex::new(Vec::new()));
            let (ping, manifest, labels, echo) = complex_tools(&log);
            let agent = client
                .agent(SESSION_MODEL)
                .preamble(COMPLEX_SESSION_PREAMBLE)
                .tool(ping)
                .tool(manifest)
                .tool(labels)
                .tool(echo)
                .additional_params(json!({"parallel_tool_calls": false}))
                .default_max_turns(10)
                .build();
            let mut history = Vec::<Message>::new();

            let response = agent.chat(COMPLEX_SESSION_PROMPT, &mut history).await?;

            assert_contains_all_case_insensitive(
                &response,
                &["EMPTY-OK", "MANIFEST-OK", "LABELS-OK", "ESCAPE-OK"],
            );
            assert_complex_invocations(&log);
            assert_history_records_sequential_tool_roundtrips(
                &history,
                &[
                    PingEmpty::NAME,
                    InspectManifest::NAME,
                    JoinLabels::NAME,
                    EscapeEcho::NAME,
                ],
            );

            Ok(())
        },
    )
    .await
}

#[tokio::test]
async fn sequential_complex_tool_calls_streaming() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/sequential_complex_tool_calls_streaming",
        |client| async move {
            let log = Arc::new(Mutex::new(Vec::new()));
            let (ping, manifest, labels, echo) = complex_tools(&log);
            let agent = client
                .agent(SESSION_MODEL)
                .preamble(COMPLEX_SESSION_PREAMBLE)
                .tool(ping)
                .tool(manifest)
                .tool(labels)
                .tool(echo)
                .additional_params(json!({"parallel_tool_calls": false}))
                .build();

            let mut stream = agent
                .stream_chat(COMPLEX_SESSION_PROMPT, Vec::<Message>::new())
                .max_turns(10)
                .await;
            let observation = collect_stream_observation(&mut stream).await;

            anyhow::ensure!(
                observation.errors.is_empty(),
                "stream should not emit errors: {:?}",
                observation.errors
            );
            let expected_tool_calls = vec![
                PingEmpty::NAME.to_string(),
                InspectManifest::NAME.to_string(),
                JoinLabels::NAME.to_string(),
                EscapeEcho::NAME.to_string(),
            ];
            anyhow::ensure!(
                observation.tool_calls == expected_tool_calls,
                "stream should expose the same ordered tool calls as non-streaming; saw {:?}",
                observation.tool_calls
            );
            anyhow::ensure!(
                observation.tool_results == 4,
                "expected 4 streamed tool results, saw {}",
                observation.tool_results
            );
            anyhow::ensure!(
                observation.got_final_response,
                "stream should emit a final response"
            );
            let response = observation
                .final_response_text
                .as_deref()
                .ok_or_else(|| anyhow::anyhow!("stream should produce final response text"))?;
            assert_contains_all_case_insensitive(
                response,
                &["EMPTY-OK", "MANIFEST-OK", "LABELS-OK", "ESCAPE-OK"],
            );
            assert_complex_invocations(&log);

            Ok(())
        },
    )
    .await
}

#[tokio::test]
async fn parallel_tool_calls_single_turn_nonstreaming() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/parallel_tool_calls_single_turn_nonstreaming",
        |client| async move {
            let agent = client
                .agent(SESSION_MODEL)
                .preamble(TWO_TOOL_STREAM_PREAMBLE)
                .tool(AlphaSignal)
                .tool(BetaSignal)
                .default_max_turns(5)
                .build();
            let mut history = Vec::<Message>::new();

            let response = agent.chat(TWO_TOOL_STREAM_PROMPT, &mut history).await?;

            assert_contains_all_case_insensitive(
                &response,
                &[ALPHA_SIGNAL_OUTPUT, BETA_SIGNAL_OUTPUT],
            );
            let calls = history_tool_calls(&history);
            let call_names = calls
                .iter()
                .map(|call| call.name_or_id.as_str())
                .collect::<Vec<_>>();
            anyhow::ensure!(
                calls.len() == 2
                    && call_names.contains(&AlphaSignal::NAME)
                    && call_names.contains(&BetaSignal::NAME),
                "expected both zero-argument tools in one model turn, saw {:?}",
                call_names
            );
            anyhow::ensure!(
                calls[0].message_index == calls[1].message_index,
                "parallel tool calls should be recorded on one assistant message"
            );
            let result_count = history_tool_results(&history).len();
            anyhow::ensure!(
                result_count == 2,
                "expected two tool results, saw {result_count}"
            );

            Ok(())
        },
    )
    .await
}

#[tokio::test]
async fn parallel_tool_calls_single_turn_streaming() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/parallel_tool_calls_single_turn_streaming",
        |client| async move {
            let agent = client
                .agent(SESSION_MODEL)
                .preamble(TWO_TOOL_STREAM_PREAMBLE)
                .tool(AlphaSignal)
                .tool(BetaSignal)
                .build();

            let mut stream = agent
                .stream_prompt(TWO_TOOL_STREAM_PROMPT)
                .max_turns(5)
                .await;
            let observation = collect_stream_observation(&mut stream).await;

            assert_two_tool_roundtrip_contract(
                &observation,
                &[AlphaSignal::NAME, BetaSignal::NAME],
                &[ALPHA_SIGNAL_OUTPUT, BETA_SIGNAL_OUTPUT],
            );

            Ok(())
        },
    )
    .await
}

#[tokio::test]
async fn raw_stream_complex_tool_call_deltas_have_object_arguments() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/raw_stream_complex_tool_call_deltas_have_object_arguments",
        |client| async move {
            let log = Arc::new(Mutex::new(Vec::new()));
            let model = client.completion_model(SESSION_MODEL);
            let tool = InspectManifest { log };
            let request = model
                .completion_request(
                    "Call inspect_manifest exactly once for project rig-openrouter with critical=true, retries=2, \
                     steps [{name: plan, weight: 1}, {name: verify, weight: 2}], and note `streamed nested JSON`. \
                     Do not write normal text before the tool call.",
                )
                .preamble("Use the requested tool call and no prose before it.".to_string())
                .tool(rig::tool::tool_definition(&tool))
                .tool_choice(ToolChoice::Required)
                .build();

            let observation = collect_raw_stream_observation(model.stream(request).await?).await;

            assert_raw_stream_tool_call_arguments_are_objects(
                &observation,
                &[InspectManifest::NAME],
            );
            let tool_call = observation
                .tool_calls
                .iter()
                .find(|tool_call| tool_call.function.name == InspectManifest::NAME)
                .ok_or_else(|| anyhow::anyhow!("raw stream should emit inspect_manifest"))?;
            anyhow::ensure!(tool_call.function.arguments["project"] == "rig-openrouter");
            anyhow::ensure!(tool_call.function.arguments["flags"]["critical"] == true);
            anyhow::ensure!(
                tool_call.function.arguments["steps"].as_array().map(Vec::len) == Some(2)
            );

            Ok(())
        },
    )
    .await
}

#[tokio::test]
async fn long_history_replay_with_tool_result_continuation() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/long_history_replay_with_tool_result_continuation",
        |client| async move {
            let model = client.completion_model(SESSION_MODEL);
            let request = model
                .completion_request(
                    "Answer in one short sentence: what is my favorite color, which label came from the tool, \
                     and which release lane did I choose? Do not call any tools.",
                )
                .preamble("You are concise and should rely on the provided chat history.".to_string())
                .message(Message::user("My favorite color is teal. Please remember it."))
                .message(Message::assistant("Noted: your favorite color is teal."))
                .message(Message::user("For this release, use the canary lane."))
                .message(Message::assistant("Understood: the release lane is canary."))
                .message(Message::user("Look up the harbor label with the tool."))
                .message(Message::Assistant {
                    id: None,
                    content: OneOrMany::one(AssistantContent::tool_call(
                        "call_REDACTED_1",
                        AlphaSignal::NAME,
                        json!({}),
                    )),
                })
                .message(Message::tool_result("call_REDACTED_1", ALPHA_SIGNAL_OUTPUT))
                .message(Message::assistant("The harbor label is crimson-harbor."))
                .tool(rig::tool::tool_definition(&AlphaSignal))
                .tool_choice(ToolChoice::None)
                .build();

            let response = model.completion(request).await?;
            let text = response
                .choice
                .iter()
                .filter_map(|content| match content {
                    AssistantContent::Text(text) => Some(text.text.as_str()),
                    _ => None,
                })
                .collect::<String>();

            assert_contains_all_case_insensitive(&text, &["teal", ALPHA_SIGNAL_OUTPUT, "canary"]);
            anyhow::ensure!(
                response.usage.input_tokens > 0 && response.usage.output_tokens > 0,
                "usage should be populated on long-history replay: {:?}",
                response.usage
            );
            anyhow::ensure!(
                response
                    .raw_response
                    .choices
                    .iter()
                    .all(|choice| choice.finish_reason.is_some()),
                "raw response should preserve finish reasons"
            );
            assert_nonempty_response(&response.raw_response.model);

            Ok(())
        },
    )
    .await
}

#[derive(Debug, Deserialize, JsonSchema, Serialize)]
struct NestedPlan {
    release: ReleaseInfo,
    checks: Vec<PlanCheck>,
}

#[derive(Debug, Deserialize, JsonSchema, Serialize)]
struct ReleaseInfo {
    lane: String,
    risk: String,
}

#[derive(Debug, Deserialize, JsonSchema, Serialize)]
struct PlanCheck {
    name: String,
    required: bool,
}

#[tokio::test]
async fn nested_structured_output_schema_roundtrip() -> Result<()> {
    with_openrouter_cassette_result(
        "agent_tool_sessions/nested_structured_output_schema_roundtrip",
        |client| async move {
            let agent = client
                .agent(STRUCTURED_MODEL)
                .preamble(
                    "Return only data that satisfies the requested schema. Use lane canary, risk low, \
                     and checks compile=true and replay=true.",
                )
                .additional_params(json!({
                    "provider": {
                        "require_parameters": true,
                        "order": ["Google AI Studio", "Google Vertex"]
                    }
                }))
                .build();

            let plan: NestedPlan = agent
                .prompt_typed("Create the OpenRouter cassette release validation plan.")
                .await?;

            anyhow::ensure!(plan.release.lane.eq_ignore_ascii_case("canary"));
            anyhow::ensure!(plan.release.risk.eq_ignore_ascii_case("low"));
            anyhow::ensure!(
                plan.checks
                    .iter()
                    .any(|check| check.name.eq_ignore_ascii_case("compile") && check.required),
                "structured output should include the compile check"
            );
            anyhow::ensure!(
                plan.checks
                    .iter()
                    .any(|check| check.name.eq_ignore_ascii_case("replay") && check.required),
                "structured output should include the replay check"
            );

            Ok(())
        },
    )
    .await
}


// Copyright (c) 2024 Justin Andreas Lacoste (@27justin)
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the
// use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it
// freely, subject to the following restrictions:
//
//     1. The origin of this software must not be misrepresented; you must not
//     claim that you wrote the original software. If you use this software in a
//     product, an acknowledgment in the product documentation would be
//     appreciated but is not required.
//
//     2. Altered source versions must be plainly marked as such, and must not
//     be misrepresented as being the original software.
//
//     3. This notice may not be removed or altered from any source
//     distribution.
//
// SPDX-License-Identifier: Zlib

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define CLAY_IMPLEMENTATION
#include "../../clay.h"

#include <cairo/cairo.h>

////////////////////////////////
//
// Public API
//

// Initialize the internal cairo pointer with the user provided instance.
// This is REQUIRED before calling Clay_Cairo_Render.
void Clay_Cairo_Initialize(cairo_t *cairo);

// Render the command queue to the `cairo_t*` instance you called
// `Clay_Cairo_Initialize` on.
void Clay_Cairo_Render(Clay_RenderCommandArray commands, char** fonts);
////////////////////////////////

////////////////////////////////
// Convencience macros
//
#define CLAY_TO_CAIRO(color) color.r / 255.0, color.g / 255.0, color.b / 255.0, color.a / 255.0
#define DEG2RAD(degrees) (degrees * ( M_PI / 180.0 ) )
////////////////////////////////

////////////////////////////////
// Implementation
//

// Cairo instance
static cairo_t *Clay__Cairo = NULL;

// Return a null-terminated copy of Clay_String `str`.
// Callee is required to free.
static inline char *Clay_Cairo__NullTerminate(Clay_String *str) {
	char *copy = (char*) malloc(str->length + 1);
	if (!copy) {
		fprintf(stderr, "Memory allocation failed\n");
		return NULL;
	}
	memcpy(copy, str->chars, str->length);
	copy[str->length] = '\0';
	return copy;
}

// Measure text using cairo's *toy* text API.
static inline Clay_Dimensions Clay_Cairo_MeasureText(Clay_StringSlice str, Clay_TextElementConfig *config, void *userData) {
	// Edge case: Clay computes the width of a whitespace character
	// once.  Cairo does not factor in whitespaces when computing text
	// extents, this edge-case serves as a short-circuit to introduce
	// (somewhat) sensible values into Clay.
    char** fonts = (char**)userData;
	if(str.length == 1 && str.chars[0] == ' ') {
		cairo_text_extents_t te;
		cairo_text_extents(Clay__Cairo, " ", &te);
		return (Clay_Dimensions) {
			// The multiplication here follows no real logic, just
			// brute-forcing it until the text boundaries look
			// okay-ish.  You should probably rather use a proper text
			// shaping engine like HarfBuzz or Pango.
			.width = ((float) te.x_advance) * 1.9f,
			.height = (float) config->fontSize
		};
	}

	// Ensure string is null-terminated for Cairo
    Clay_String toTerminate = (Clay_String){ .chars = str.chars, .length = str.length, .isStaticallyAllocated = false };
	char *text = Clay_Cairo__NullTerminate(&toTerminate);
	char *font_family = fonts[config->fontId];

	// Save and reset the Cairo context to avoid unwanted transformations
	cairo_save(Clay__Cairo);
	cairo_identity_matrix(Clay__Cairo);

	// Set font properties
	cairo_select_font_face(Clay__Cairo, font_family, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
	cairo_set_font_size(Clay__Cairo, config->fontSize);

	// Use glyph extents for better precision
	cairo_scaled_font_t *scaled_font = cairo_get_scaled_font(Clay__Cairo);
	if (!scaled_font) {
		fprintf(stderr, "Failed to get scaled font\n");
		cairo_restore(Clay__Cairo);
		free(text);
		return (Clay_Dimensions){0, 0};
	}

	cairo_glyph_t *glyphs = NULL;
	int num_glyphs = 0;
	cairo_status_t status = cairo_scaled_font_text_to_glyphs(
		scaled_font, 0, 0, text, -1, &glyphs, &num_glyphs, NULL, NULL, NULL
	);

	if (status != CAIRO_STATUS_SUCCESS || !glyphs || num_glyphs == 0) {
		fprintf(stderr, "Failed to generate glyphs: %s\n", cairo_status_to_string(status));
		cairo_restore(Clay__Cairo);
		free(text);
		return (Clay_Dimensions){0, 0};
	}

	// Measure the glyph extents
	cairo_text_extents_t glyph_extents;
	cairo_glyph_extents(Clay__Cairo, glyphs, num_glyphs, &glyph_extents);

	// Clean up glyphs
	cairo_glyph_free(glyphs);

	// Restore the Cairo context
	cairo_restore(Clay__Cairo);

	// Free temporary strings
	free(text);

	// Return dimensions
	return (Clay_Dimensions){
		.width =  (float) glyph_extents.width,
		.height = (float) glyph_extents.height
	};
}

void Clay_Cairo_Initialize(cairo_t *cairo) {
	Clay__Cairo = cairo;
}

// Internally used to copy images onto our document/active workspace.
void Clay_Cairo__Blit_Surface(cairo_surface_t *src_surface, cairo_surface_t *dest_surface,
							  double x, double y, double scale_x, double scale_y) {
	// Create a cairo context for the destination surface
	cairo_t *cr = cairo_create(dest_surface);

	// Save the context's state
	cairo_save(cr);

	// Apply translation to position the source at (x, y)
	cairo_translate(cr, x, y);

	// Apply scaling to the context
	cairo_scale(cr, scale_x, scale_y);

	// Set the source surface at (0, 0) after applying transformations
	cairo_set_source_surface(cr, src_surface, 0, 0);

	// Paint the scaled source surface onto the destination surface
	cairo_paint(cr);

	// Restore the context's state to remove transformations
	cairo_restore(cr);

	// Clean up
	cairo_destroy(cr);
}

void Clay_Cairo_Render(Clay_RenderCommandArray commands, char** fonts) {
	cairo_t *cr = Clay__Cairo;
	for(size_t i = 0; i < commands.length; i++) {
		Clay_RenderCommand *command = Clay_RenderCommandArray_Get(&commands, i);

		switch(command->commandType) {
		case CLAY_RENDER_COMMAND_TYPE_RECTANGLE: {
            Clay_RectangleRenderData *config = &command->renderData.rectangle;
			Clay_BoundingBox bb = command->boundingBox;

			cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->backgroundColor));

			cairo_new_sub_path(cr);
			cairo_arc(cr, bb.x + config->cornerRadius.topLeft,
					  bb.y + config->cornerRadius.topLeft,
					  config->cornerRadius.topLeft,
					  M_PI, 3 * M_PI / 2); // 180° to 270°
			cairo_arc(cr, bb.x + bb.width - config->cornerRadius.topRight,
					  bb.y + config->cornerRadius.topRight,
					  config->cornerRadius.topRight,
					  3 * M_PI / 2, 2 * M_PI); // 270° to 360°
			cairo_arc(cr, bb.x + bb.width - config->cornerRadius.bottomRight,
					  bb.y + bb.height - config->cornerRadius.bottomRight,
					  config->cornerRadius.bottomRight,
					  0, M_PI / 2); // 0° to 90°
			cairo_arc(cr, bb.x + config->cornerRadius.bottomLeft,
					  bb.y + bb.height - config->cornerRadius.bottomLeft,
					  config->cornerRadius.bottomLeft,
					  M_PI / 2, M_PI); // 90° to 180°
			cairo_close_path(cr);

			cairo_fill(cr);
			break;
		}
		case CLAY_RENDER_COMMAND_TYPE_TEXT: {
			// Cairo expects null terminated strings, we need to clone
			// to temporarily introduce one.
            Clay_TextRenderData *config = &command->renderData.text;
            Clay_String toTerminate = (Clay_String){ .chars = config->stringContents.chars, .length = config->stringContents.length, .isStaticallyAllocated = false };
			char *text = Clay_Cairo__NullTerminate(&toTerminate);
			char *font_family = fonts[config->fontId];

			Clay_BoundingBox bb = command->boundingBox;
			Clay_Color color = config->textColor;

			cairo_select_font_face(Clay__Cairo, font_family, CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
			cairo_set_font_size(cr, config->fontSize);

			cairo_move_to(cr, bb.x, bb.y + bb.height);

			cairo_set_source_rgba(cr, CLAY_TO_CAIRO(color));
			cairo_show_text(cr, text);
			cairo_close_path(cr);

			free(text);
			break;
		}
		case CLAY_RENDER_COMMAND_TYPE_BORDER: {
            Clay_BorderRenderData *config = &command->renderData.border;
			Clay_BoundingBox bb = command->boundingBox;

			double top_left_radius = config->cornerRadius.topLeft / 2.0;
			double top_right_radius = config->cornerRadius.topRight / 2.0;
			double bottom_right_radius = config->cornerRadius.bottomRight / 2.0;
			double bottom_left_radius = config->cornerRadius.bottomLeft / 2.0;

			// Draw the top border
			if (config->width.top > 0) {
				cairo_set_line_width(cr, config->width.top);
				cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->color));

				cairo_new_sub_path(cr);

				// Left half-arc for top-left corner
				cairo_arc(cr, bb.x + top_left_radius, bb.y + top_left_radius, top_left_radius, DEG2RAD(225), DEG2RAD(270));

				// Line to right half-arc
				cairo_line_to(cr, bb.x + bb.width - top_right_radius, bb.y);

				// Right half-arc for top-right corner
				cairo_arc(cr, bb.x + bb.width - top_right_radius, bb.y + top_right_radius, top_right_radius, DEG2RAD(270), DEG2RAD(305));

				cairo_stroke(cr);
			}

			// Draw the right border
			if (config->width.right > 0) {
				cairo_set_line_width(cr, config->width.right);
				cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->color));

				cairo_new_sub_path(cr);

				// Top half-arc for top-right corner
				cairo_arc(cr, bb.x + bb.width - top_right_radius, bb.y + top_right_radius, top_right_radius, DEG2RAD(305), DEG2RAD(350));

				// Line to bottom half-arc
				cairo_line_to(cr, bb.x + bb.width, bb.y + bb.height - bottom_right_radius);

				// Bottom half-arc for bottom-right corner
				cairo_arc(cr, bb.x + bb.width - bottom_right_radius, bb.y + bb.height - bottom_right_radius, bottom_right_radius, DEG2RAD(0), DEG2RAD(45));

				cairo_stroke(cr);
			}

			// Draw the bottom border
			if (config->width.bottom > 0) {
				cairo_set_line_width(cr, config->width.bottom);
				cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->color));

				cairo_new_sub_path(cr);

				// Right half-arc for bottom-right corner
				cairo_arc(cr, bb.x + bb.width - bottom_right_radius, bb.y + bb.height - bottom_right_radius, bottom_right_radius, DEG2RAD(45), DEG2RAD(90));

				// Line to left half-arc
				cairo_line_to(cr, bb.x + bottom_left_radius, bb.y + bb.height);

				// Left half-arc for bottom-left corner
				cairo_arc(cr, bb.x + bottom_left_radius, bb.y + bb.height - bottom_left_radius, bottom_left_radius, DEG2RAD(90), DEG2RAD(135));

				cairo_stroke(cr);
			}

			// Draw the left border
			if (config->width.left > 0) {
				cairo_set_line_width(cr, config->width.left);
				cairo_set_source_rgba(cr, CLAY_TO_CAIRO(config->color));

				cairo_new_sub_path(cr);

				// Bottom half-arc for bottom-left corner
				cairo_arc(cr, bb.x + bottom_left_radius, bb.y + bb.height - bottom_left_radius, bottom_left_radius, DEG2RAD(135), DEG2RAD(180));

				// Line to top half-arc
				cairo_line_to(cr, bb.x, bb.y + top_left_radius);

				// Top half-arc for top-left corner
				cairo_arc(cr, bb.x + top_left_radius, bb.y + top_left_radius, top_left_radius, DEG2RAD(180), DEG2RAD(225));

				cairo_stroke(cr);
			}
			break;
		}
		case CLAY_RENDER_COMMAND_TYPE_IMAGE: {
            Clay_ImageRenderData *config = &command->renderData.image;
			Clay_BoundingBox bb = command->boundingBox;

			char *path = config->imageData;

			cairo_surface_t *surf = cairo_image_surface_create_from_png(path),
							*origin = cairo_get_target(cr);

			// Calculate the original image dimensions
			double image_w = cairo_image_surface_get_width(surf),
				image_h = cairo_image_surface_get_height(surf);

			// Calculate the scaling factor to fit within the bounding box while preserving aspect ratio
			double scale_w = bb.width / image_w;
			double scale_h = bb.height / image_h;
			double scale = (scale_w < scale_h) ? scale_w : scale_h; // Use the smaller scaling factor

			// Apply the same scale to both dimensions to preserve aspect ratio
			double scale_x = scale;
			double scale_y = scale;

			// Calculate the scaled image dimensions
			double scaled_w = image_w * scale_x;
			double scaled_h = image_h * scale_y;

			// Adjust the x and y coordinates to center the scaled image within the bounding box
			double centered_x = bb.x + (bb.width - scaled_w) / 2.0;
			double centered_y = bb.y + (bb.height - scaled_h) / 2.0;

			// Blit the scaled and centered image
			Clay_Cairo__Blit_Surface(surf, origin, centered_x, centered_y, scale_x, scale_y);

			// Clean up the source surface
			cairo_surface_destroy(surf);
			break;
		}
		case CLAY_RENDER_COMMAND_TYPE_CUSTOM: {
			// Slot your custom elements in here.
		}
		default: {
			fprintf(stderr, "Unknown command type %d\n", (int) command->commandType);
		}
		}
	}
}


#!/usr/bin/env bash
set -euo pipefail
set +x
umask 077

usage() {
  cat <<'USAGE'
Usage:
  publish-package.sh [--vault VAULT] [--item ITEM] [--account ACCOUNT] [--access ACCESS] [--tag TAG]

Publishes the package in the current directory through a temporary authenticated
npmrc. Must run inside the persistent tmux session used for 1Password access.
Defaults to the Molty service-account item; --account opts into an interactive
desktop-vault fallback.
USAGE
}

VAULT="${NPM_OP_VAULT:-Molty}"
ITEM="${NPM_OP_ITEM:-npm Registry - steipete - Release Automation}"
ITEM_EXPLICIT=0
if [ -n "${NPM_OP_ITEM:-}" ]; then
  ITEM_EXPLICIT=1
fi
ACCOUNT=""
REGISTRY="${NPM_REGISTRY:-https://registry.npmjs.org/}"
ACCESS="public"
TAG="latest"

while [ "$#" -gt 0 ]; do
  case "$1" in
    --vault) VAULT="${2:?missing vault}"; shift 2 ;;
    --item) ITEM="${2:?missing item}"; ITEM_EXPLICIT=1; shift 2 ;;
    --account) ACCOUNT="${2:?missing account}"; shift 2 ;;
    --access) ACCESS="${2:?missing access}"; shift 2 ;;
    --tag) TAG="${2:?missing tag}"; shift 2 ;;
    -h|--help) usage; exit 0 ;;
    *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
  esac
done

# Desktop fallback keeps the legacy item name unless one was named explicitly.
if [ -n "$ACCOUNT" ] && [ "$ITEM_EXPLICIT" -eq 0 ]; then
  ITEM="npmjs"
fi

if [ -z "${TMUX:-}" ]; then
  echo "refusing to run: npm auth must stay inside one persistent tmux session" >&2
  exit 2
fi

for bin in op jq node npm; do
  command -v "$bin" >/dev/null 2>&1 || { echo "missing required binary: $bin" >&2; exit 2; }
done
test -f package.json || { echo "package.json not found in current directory" >&2; exit 2; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGE_DIR="$PWD"
WORK="$(mktemp -d /tmp/npm-publish.XXXXXX)"
NPMRC="$WORK/npmrc"
# shellcheck disable=SC2329 # invoked via trap EXIT
cleanup() {
  rm -rf "$WORK"
  unset ITEM_JSON NPM_OTP
}
trap cleanup EXIT

name="$(node -p 'require("./package.json").name')"
version="$(node -p 'require("./package.json").version')"
if npm view "$name@$version" version >/dev/null 2>&1; then
  echo "$name@$version is already published" >&2
  exit 5
fi

# shellcheck source=npm-auth.sh
source "$SCRIPT_DIR/npm-auth.sh"

resolve_op_item
ensure_npm_auth
unset ITEM_JSON

who="$(npm_auth_whoami 2>"$WORK/npm-whoami.log" || true)"
if [ -z "$who" ]; then
  echo "npm auth check failed" >&2
  redact <"$WORK/npm-whoami.log" >&2
  exit 4
fi
echo "npm auth ok as $who"

publish_log="$WORK/npm-publish.log"
otp="$(fresh_command_otp)"
if ! NPM_CONFIG_OTP="$otp" npm_authenticated publish "$PACKAGE_DIR" --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1; then
  if grep -qiE 'otp|one-time|two-factor|2fa|EOTP' "$publish_log"; then
    echo "publish OTP expired; retrying once with a fresh OTP" >&2
    sleep 31
    otp="$(current_otp)"
    NPM_CONFIG_OTP="$otp" npm_authenticated publish "$PACKAGE_DIR" --access "$ACCESS" --tag "$TAG" >"$publish_log" 2>&1 || {
      redact <"$publish_log" >&2
      exit 6
    }
  else
    redact <"$publish_log" >&2
    exit 6
  fi
fi
redact <"$publish_log"

for _ in {1..12}; do
  published="$(npm view "$name@$version" version 2>/dev/null || true)"
  if [ "$published" = "$version" ]; then
    echo "registry version verified: $name@$published"
    exit 0
  fi
  sleep 5
done
echo "registry did not expose $name@$version in time" >&2
exit 7


"""Test suite for list-based impersonate parameter functionality."""
import pytest
import pytest_httpbin
from unittest.mock import patch, MagicMock

from scrapling import Fetcher
from scrapling.fetchers import FetcherSession
from scrapling.engines.static import _select_random_browser

class TestRandomBrowserSelection:
    """Test the random browser selection helper function."""

    def test_select_random_browser_with_single_string(self):
        """Test that single browser string is returned as-is."""
        result = _select_random_browser("chrome")
        assert result == "chrome"

    def test_select_random_browser_with_none(self):
        """Test that None is returned as-is."""
        result = _select_random_browser(None)
        assert result is None

    def test_select_random_browser_with_list(self):
        """Test that a browser is randomly selected from a list."""
        browsers = ["chrome", "firefox", "safari"]
        result = _select_random_browser(browsers)
        assert result in browsers

    def test_select_random_browser_with_empty_list(self):
        """Test that empty list returns None."""
        result = _select_random_browser([])
        assert result is None

    def test_select_random_browser_with_single_item_list(self):
        """Test that single-item list returns that item."""
        result = _select_random_browser(["chrome"])
        assert result == "chrome"

@pytest_httpbin.use_class_based_httpbin
class TestFetcherWithImpersonateList:
    """Test Fetcher with list-based impersonate parameter."""

    @pytest.fixture(autouse=True)
    def setup_urls(self, httpbin):
        """Fixture to set up URLs for testing."""
        self.basic_url = f"{httpbin.url}/get"

    def test_get_with_impersonate_list(self):
        """Test that GET request works with impersonate as a list."""
        browsers = ["chrome", "firefox"]
        response = Fetcher.get(self.basic_url, impersonate=browsers)
        assert response.status == 200

    def test_get_with_single_impersonate(self):
        """Test that GET request still works with single browser string."""
        response = Fetcher.get(self.basic_url, impersonate="chrome")
        assert response.status == 200

    def test_post_with_impersonate_list(self):
        """Test that POST request works with impersonate as a list."""
        browsers = ["chrome", "firefox"]
        post_url = self.basic_url.replace("/get", "/post")
        response = Fetcher.post(post_url, data={"key": "value"}, impersonate=browsers)
        assert response.status == 200

    def test_put_with_impersonate_list(self):
        """Test that PUT request works with impersonate as a list."""
        browsers = ["chrome", "safari"]
        put_url = self.basic_url.replace("/get", "/put")
        response = Fetcher.put(put_url, data={"key": "value"}, impersonate=browsers)
        assert response.status == 200

    def test_delete_with_impersonate_list(self):
        """Test that DELETE request works with impersonate as a list."""
        browsers = ["chrome", "edge"]
        delete_url = self.basic_url.replace("/get", "/delete")
        response = Fetcher.delete(delete_url, impersonate=browsers)
        assert response.status == 200

@pytest_httpbin.use_class_based_httpbin
class TestFetcherSessionWithImpersonateList:
    """Test FetcherSession with list-based impersonate parameter."""

    @pytest.fixture(autouse=True)
    def setup_urls(self, httpbin):
        """Fixture to set up URLs for testing."""
        self.basic_url = f"{httpbin.url}/get"

    def test_session_init_with_impersonate_list(self):
        """Test that FetcherSession can be initialized with impersonate as a list."""
        browsers = ["chrome", "firefox", "safari"]
        session = FetcherSession(impersonate=browsers)
        assert session._default_impersonate == browsers

    def test_session_request_with_impersonate_list(self):
        """Test that session request works with impersonate as a list."""
        browsers = ["chrome", "firefox"]
        with FetcherSession(impersonate=browsers) as session:
            response = session.get(self.basic_url)
            assert response.status == 200

    def test_session_multiple_requests_with_impersonate_list(self):
        """Test that multiple requests in a session work with impersonate list."""
        browsers = ["chrome110", "chrome120", "chrome131"]
        with FetcherSession(impersonate=browsers) as session:
            response1 = session.get(self.basic_url)
            response2 = session.get(self.basic_url)
            assert response1.status == 200
            assert response2.status == 200

    def test_session_request_level_impersonate_override(self):
        """Test that request-level impersonate overrides session-level."""
        session_browsers = ["chrome", "firefox"]
        request_browser = "safari"

        with FetcherSession(impersonate=session_browsers) as session:
            response = session.get(self.basic_url, impersonate=request_browser)
            assert response.status == 200

    def test_session_request_level_impersonate_list_override(self):
        """Test that request-level impersonate list overrides session-level."""
        session_browsers = ["chrome", "firefox"]
        request_browsers = ["safari", "edge"]

        with FetcherSession(impersonate=session_browsers) as session:
            response = session.get(self.basic_url, impersonate=request_browsers)
            assert response.status == 200

class TestImpersonateTypeValidation:
    """Test type validation for impersonate parameter."""

    def test_impersonate_accepts_string(self):
        """Test that impersonate accepts string type."""
        # This should not raise any type errors
        session = FetcherSession(impersonate="chrome")
        assert session._default_impersonate == "chrome"

    def test_impersonate_accepts_list(self):
        """Test that impersonate accepts list type."""
        # This should not raise any type errors
        browsers = ["chrome", "firefox"]
        session = FetcherSession(impersonate=browsers)
        assert session._default_impersonate == browsers

    def test_impersonate_accepts_none(self):
        """Test that impersonate accepts None."""
        # This should not raise any type errors
        session = FetcherSession(impersonate=None)
        assert session._default_impersonate is None


// Copyright (c) Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)

#ifndef METAGEN_H
#define METAGEN_H

////////////////////////////////
//~ rjf: Message Type

typedef struct MG_Msg MG_Msg;
struct MG_Msg
{
  String8 location;
  String8 kind;
  String8 msg;
};

typedef struct MG_MsgNode MG_MsgNode;
struct MG_MsgNode
{
  MG_MsgNode *next;
  MG_Msg v;
};

typedef struct MG_MsgList MG_MsgList;
struct MG_MsgList
{
  MG_MsgNode *first;
  MG_MsgNode *last;
  U64 count;
};

////////////////////////////////
//~ rjf: Parse Artifact Types

typedef struct MG_FileParse MG_FileParse;
struct MG_FileParse
{
  MD_Node *root;
};

typedef struct MG_FileParseNode MG_FileParseNode;
struct MG_FileParseNode
{
  MG_FileParseNode *next;
  MG_FileParse v;
};

typedef struct MG_FileParseList MG_FileParseList;
struct MG_FileParseList
{
  MG_FileParseNode *first;
  MG_FileParseNode *last;
  U64 count;
};

////////////////////////////////
//~ rjf: Map Type

typedef struct MG_MapNode MG_MapNode;
struct MG_MapNode
{
  MG_MapNode *next;
  String8 key;
  void *val;
};

typedef struct MG_MapSlot MG_MapSlot;
struct MG_MapSlot
{
  MG_MapNode *first;
  MG_MapNode *last;
};

typedef struct MG_Map MG_Map;
struct MG_Map
{
  MG_MapSlot *slots;
  U64 slots_count;
};

////////////////////////////////
//~ rjf: String Expression Types

typedef enum MG_StrExprOpKind
{
  MG_StrExprOpKind_Null,
  MG_StrExprOpKind_Prefix,
  MG_StrExprOpKind_Postfix,
  MG_StrExprOpKind_Binary,
  MG_StrExprOpKind_COUNT
}
MG_StrExprOpKind;

typedef enum MG_StrExprOp
{
  MG_StrExprOp_Null,

#define MG_StrExprOp_FirstString MG_StrExprOp_Dot
  MG_StrExprOp_Dot,
  MG_StrExprOp_ExpandIfTrue,
  MG_StrExprOp_Concat,
  MG_StrExprOp_BumpToColumn,
#define MG_StrExprOp_LastString MG_StrExprOp_BumpToColumn

#define MG_StrExprOp_FirstNumeric MG_StrExprOp_Add
  MG_StrExprOp_Add,
  MG_StrExprOp_Subtract,
  MG_StrExprOp_Multiply,
  MG_StrExprOp_Divide,
  MG_StrExprOp_Modulo,
  MG_StrExprOp_LeftShift,
  MG_StrExprOp_RightShift,
  MG_StrExprOp_BitwiseAnd,
  MG_StrExprOp_BitwiseOr,
  MG_StrExprOp_BitwiseXor,
  MG_StrExprOp_BitwiseNegate,
  MG_StrExprOp_BooleanAnd,
  MG_StrExprOp_BooleanOr,
  MG_StrExprOp_BooleanNot,
  MG_StrExprOp_Equals,
  MG_StrExprOp_DoesNotEqual,
#define MG_StrExprOp_LastNumeric MG_StrExprOp_DoesNotEqual

  MG_StrExprOp_COUNT,
}
MG_StrExprOp;

typedef struct MG_StrExpr MG_StrExpr;
struct MG_StrExpr
{
  MG_StrExpr *parent;
  MG_StrExpr *left;
  MG_StrExpr *right;
  MG_StrExprOp op;
  MD_Node *node;
};

typedef struct MG_StrExprParseResult MG_StrExprParseResult;
struct MG_StrExprParseResult
{
  MG_StrExpr *root;
  MD_MsgList msgs;
  MD_Node *next_node;
};

////////////////////////////////
//~ rjf: Table Generation Types

typedef struct MG_NodeArray MG_NodeArray;
struct MG_NodeArray
{
  MD_Node **v;
  U64 count;
};

typedef struct MG_NodeGrid MG_NodeGrid;
struct MG_NodeGrid
{
  U64 x_stride;
  U64 y_stride;
  MG_NodeArray cells;
  MG_NodeArray row_parents;
};

typedef enum MG_ColumnKind
{
  MG_ColumnKind_DirectCell,
  MG_ColumnKind_CheckForTag,
  MG_ColumnKind_TagChild,
  MG_ColumnKind_COUNT
}
MG_ColumnKind;

typedef struct MG_ColumnDesc MG_ColumnDesc;
struct MG_ColumnDesc
{
  String8 name;
  MG_ColumnKind kind;
  String8 tag_name;
};

typedef struct MG_ColumnDescArray MG_ColumnDescArray;
struct MG_ColumnDescArray
{
  U64 count;
  MG_ColumnDesc *v;
};

typedef struct MG_TableExpandTask MG_TableExpandTask;
struct MG_TableExpandTask
{
  MG_TableExpandTask *next;
  String8 expansion_label;
  MG_NodeGrid *grid;
  MG_ColumnDescArray column_descs;
  U64 count;
  U64 idx;
};

typedef struct MG_TableExpandInfo MG_TableExpandInfo;
struct MG_TableExpandInfo
{
  MG_TableExpandTask *first_expand_task;
  String8 missing_value_fallback;
};

////////////////////////////////
//~ rjf: Main Output Path Types

typedef struct MG_Layer MG_Layer;
struct MG_Layer
{
  String8 key;
  B32 is_library;
  String8 gen_folder_name;
  String8 h_name_override;
  String8 c_name_override;
  String8List enums;
  String8List structs;
  String8List h_functions;
  String8List h_tables;
  String8List h_catchall;
  String8List h_header;
  String8List h_footer;
  String8List c_functions;
  String8List c_tables;
  String8List c_catchall;
  String8List c_header;
  String8List c_footer;
};

typedef struct MG_LayerNode MG_LayerNode;
struct MG_LayerNode
{
  MG_LayerNode *next;
  MG_Layer v;
};

typedef struct MG_LayerSlot MG_LayerSlot;
struct MG_LayerSlot
{
  MG_LayerNode *first;
  MG_LayerNode *last;
};

typedef struct MG_State MG_State;
struct MG_State
{
  U64 slots_count;
  MG_LayerSlot *slots;
};

////////////////////////////////
//~ rjf: Globals

global Arena *mg_arena = 0;
global MG_State *mg_state = 0;
read_only global MG_StrExpr mg_str_expr_nil = {&mg_str_expr_nil, &mg_str_expr_nil, &mg_str_expr_nil};

////////////////////////////////
//~ rjf: Basic Helpers

internal U64 mg_hash_from_string(String8 string);
internal TxtPt mg_txt_pt_from_string_off(String8 string, U64 off);

////////////////////////////////
//~ rjf: Message Lists

internal void mg_msg_list_push(Arena *arena, MG_MsgList *msgs, MG_Msg *msg);

////////////////////////////////
//~ rjf: String Escaping

internal String8 mg_escaped_from_str8(Arena *arena, String8 string);

////////////////////////////////
//~ rjf: String Wrapping

internal String8List mg_wrapped_lines_from_string(Arena *arena, String8 string, U64 first_line_max_width, U64 max_width, U64 wrap_indent);

////////////////////////////////
//~ rjf: C-String-Izing

internal String8 mg_c_string_literal_from_multiline_string(String8 string);
internal String8 mg_c_array_literal_contents_from_data(String8 data);

////////////////////////////////
//~ rjf: Map Functions

internal MG_Map mg_push_map(Arena *arena, U64 slot_count);
internal void *mg_map_ptr_from_string(MG_Map *map, String8 string);
internal void mg_map_insert_ptr(Arena *arena, MG_Map *map, String8 string, void *val);

////////////////////////////////
//~ rjf: String Expression Parsing

internal MG_StrExpr *mg_push_str_expr(Arena *arena, MG_StrExprOp op, MD_Node *node);
internal MG_StrExprParseResult mg_str_expr_parse_from_first_opl__min_prec(Arena *arena, MD_Node *first, MD_Node *opl, S8 min_prec);
internal MG_StrExprParseResult mg_str_expr_parse_from_first_opl(Arena *arena, MD_Node *first, MD_Node *opl);
internal MG_StrExprParseResult mg_str_expr_parse_from_root(Arena *arena, MD_Node *root);

////////////////////////////////
//~ rjf: Table Generation Functions

internal MG_NodeArray mg_node_array_make(Arena *arena, U64 count);
internal MG_NodeArray mg_child_array_from_node(Arena *arena, MD_Node *node);
internal MG_NodeGrid mg_node_grid_make_from_node(Arena *arena, MD_Node *root);
internal MG_NodeArray mg_row_from_index(MG_NodeGrid grid, U64 index);
internal MG_NodeArray mg_column_from_index(Arena *arena, MG_NodeGrid grid, U64 index);
internal MD_Node *mg_node_from_grid_xy(MG_NodeGrid grid, U64 x, U64 y);

internal MG_ColumnDescArray mg_column_desc_array_make(Arena *arena, U64 count, MG_ColumnDesc *descs);
internal MG_ColumnDescArray mg_column_desc_array_from_tag(Arena *arena, MD_Node *tag);
internal U64 mg_column_index_from_name(MG_ColumnDescArray descs, String8 name);
internal String8 mg_string_from_row_desc_idx(MD_Node *row_parent, MG_ColumnDescArray descs, U64 idx);

internal S64 mg_eval_table_expand_expr__numeric(MG_StrExpr *expr, MG_TableExpandInfo *info);
internal void mg_eval_table_expand_expr__string(Arena *arena, MG_StrExpr *expr, MG_TableExpandInfo *info, String8List *out);
internal void mg_loop_table_column_expansion(Arena *arena, String8 strexpr, MG_TableExpandInfo *info, MG_TableExpandTask *task, String8List *out);
internal String8List mg_string_list_from_table_gen(Arena *arena, MG_Map grid_name_map, MG_Map grid_column_desc_map, String8 fallback, MD_Node *gen);

////////////////////////////////
//~ rjf: Layer Lookup Functions

internal String8 mg_layer_key_from_path(String8 path);
internal MG_Layer *mg_layer_from_key(String8 key);

#endif //METAGEN_H


/*
 * Copyright 2024-2025 the original author or authors.
 */

package io.modelcontextprotocol.client;

import java.net.URI;
import java.util.Map;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;

import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.spec.McpClientTransport;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

@Timeout(15)
public class HttpClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests {

	static String host = "http://localhost:3001";

	@SuppressWarnings("resource")
	static GenericContainer<?> container = new GenericContainer<>("docker.io/node:lts-alpine3.23")
		.withCommand("npx -y @modelcontextprotocol/server-everything@2025.12.18 streamableHttp")
		.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
		.withExposedPorts(3001)
		.waitingFor(Wait.forHttp("/").forStatusCode(404));

	private final McpSyncHttpClientRequestCustomizer requestCustomizer = mock(McpSyncHttpClientRequestCustomizer.class);

	@Override
	protected McpClientTransport createMcpTransport() {
		return HttpClientStreamableHttpTransport.builder(host).httpRequestCustomizer(requestCustomizer).build();
	}

	@BeforeAll
	static void startContainer() {
		container.start();
		int port = container.getMappedPort(3001);
		host = "http://" + container.getHost() + ":" + port;
	}

	@AfterAll
	static void stopContainer() {
		container.stop();
	}

	@Test
	void customizesRequests() {
		var mcpTransportContext = McpTransportContext.create(Map.of("some-key", "some-value"));
		withClient(createMcpTransport(), syncSpec -> syncSpec.transportContextProvider(() -> mcpTransportContext),
				mcpSyncClient -> {
					mcpSyncClient.initialize();

					verify(requestCustomizer, atLeastOnce()).customize(any(), eq("POST"), eq(URI.create(host + "/mcp")),
							eq("{\"jsonrpc\":\"2.0\",\"method\":\"notifications/initialized\"}"),
							eq(mcpTransportContext));
				});
	}

}


# Add Slash Command Support for Coding Agents

## Summary
- Enable OpenSpec to generate and update custom slash commands for supported coding agents (Claude Code and Cursor).
- Provide three slash commands aligned with OpenSpec's workflow: proposal (start a change proposal), apply (implement), and archive.
- Share slash command templating between agents to make future extensions simple.

## Motivation
Developers use different coding agents and editors. Having consistent slash commands across tools for the OpenSpec workflow reduces friction and ensures a standard way to trigger the workflow. Supporting both Claude Code and Cursor now lays a foundation for future agents that introduce slash command features.

## Proposal
1. During `openspec init`, when a user selects a supported tool, generate slash command configuration for three OpenSpec workflow stages:
   - Claude (namespaced): `/openspec/proposal`, `/openspec/apply`, `/openspec/archive`.
   - Cursor (flat, prefixed): `/openspec-proposal`, `/openspec-apply`, `/openspec-archive`.
   - Semantics:
     - Create – scaffold a change (ID, `proposal.md`, `tasks.md`, delta specs); validate strictly.
     - Apply – implement an approved change; complete tasks; validate strictly.
     - Archive – archive after deployment; update specs if needed.
   - Each command file MUST embed concise, step-by-step instructions sourced from `openspec/README.md` (see Template Content section).
2. Store slash command files per tool:
   - Claude Code: `.claude/commands/openspec/{proposal,apply,archive}.md`
   - Cursor: `.cursor/commands/{openspec-proposal,openspec-apply,openspec-archive}.md`
   - Ensure nested directories are created.
3. Command file format and metadata:
   - Use Markdown with optional YAML frontmatter for tool metadata (name/title, description, category/tags) when supported by the tool.
   - Place OpenSpec markers around the body only, never inside frontmatter.
   - Keep the visible slash name, file name, and any frontmatter `name`/`id` consistently aligned (e.g., `proposal`, `openspec-proposal`).
   - Namespacing: categorize these under “OpenSpec” and prefer unique IDs (e.g., `openspec-proposal`) to avoid collisions.
4. Centralize templates: define command bodies once and reuse across tools; apply minimal per-tool wrappers (frontmatter, categories, filenames).
5. During `openspec update`, refresh only existing slash command files (per-file basis) within markers; do not create missing files or new tools.

## Design Ideas
- Introduce `SlashCommandConfigurator` to manage multiple files per tool.
  - Expose targets rather than a single `configFileName` (e.g., `getTargets(): Array<{ path: string; kind: 'slash'; id: string }>`).
  - Provide `generateAll(projectPath, openspecDir)` for init and `updateExisting(projectPath, openspecDir)` for update.
- Per-tool adapters add only frontmatter and pathing; bodies come from shared templates.
- Templates live in `TemplateManager` with helpers that extract concise, authoritative snippets from `openspec/README.md`.
- Update flow logs per-file results so users see exactly which slash files were refreshed.

### Marker Placement
- Markers MUST wrap only the Markdown body contents:
  - Frontmatter (if present) goes first.
  - Then `<!-- OPENSPEC:START -->` … body … `<!-- OPENSPEC:END -->`.
  - Avoid inserting markers into the YAML block to prevent parse errors.

### Idempotency and Creation Rules
- `init`: create all three files for the chosen tool(s) once; subsequent `init` runs are no-ops for existing files.
- `update`: refresh only files that exist; skip missing ones without creating new files.
- Directory creation for `.claude/commands/openspec/` and `.cursor/commands/` is the configurator’s responsibility.

### Command Naming & UX
- Claude Code: use namespacing in the slash itself for readability and grouping: `/openspec/proposal`, `/openspec/apply`, `/openspec/archive`.
- Cursor: use flat names with an `openspec-` prefix: `/openspec-proposal`, `/openspec-apply`, `/openspec-archive`. Group via `category: OpenSpec` when supported.
- Consistency: align file names, visible slash names, and any frontmatter `id` (e.g., `id: openspec-apply`).
- Migration: do not rename existing commands during `update`; apply new naming only on `init` (or via an explicit migrate step).

## Open Questions
- Validate exact metadata/frontmatter supported by each tool version; if unsupported, omit frontmatter and ship Markdown body only.
- Confirm the final Cursor command file location for the targeted versions; fall back to Markdown-only if Cursor does not parse frontmatter.
- Evaluate additional commands beyond the initial three (e.g., `/show-change`, `/validate-all`) based on user demand.

## Alternatives
- Hard-code slash command text per tool (rejected: duplicates content; increases maintenance).
- Delay Cursor support until its config stabilizes (partial accept): gate Cursor behind a feature flag until verified in real environments.

## Risks
- Tool configuration formats may change, requiring updates to wrappers/frontmatter.
- Incorrect paths or categories can hide commands; add path existence checks and clear logging.
- Marker misuse (inside frontmatter) can break parsing; enforce placement rules in tests.

## Future Work
- Support additional editors/agents that expose slash command APIs.
- Allow users to customize command names and categories during `openspec init`.
- Provide a dedicated command to regenerate slash commands without running full `update`.

## File Format Examples
The following examples illustrate expected structure. If a tool does not support frontmatter, omit the YAML block and keep only the markers + body.

### Claude Code: `.claude/commands/openspec/proposal.md`
```markdown
---
name: OpenSpec: Proposal
description: Scaffold a new OpenSpec change and validate strictly.
category: OpenSpec
tags: [openspec, change]
---
<!-- OPENSPEC:START -->
...command body from shared template...
<!-- OPENSPEC:END -->
```

Slash invocation: `/openspec/proposal` (namespaced)

### Cursor: `.cursor/commands/openspec-proposal.md`
```markdown
---
name: /openspec-proposal
id: openspec-proposal
category: OpenSpec
description: Scaffold a new OpenSpec change and validate strictly.
---
<!-- OPENSPEC:START -->
...command body from shared template...
<!-- OPENSPEC:END -->
```

Slash invocation: `/openspec-proposal` (flat, prefixed)

## Template Content
Templates should be brief, actionable, and sourced from `openspec/README.md` to avoid duplication. Each command body includes:
- Guardrails: ask 1–2 clarifying questions if needed; follow minimal-complexity rules; use `pnpm` for Node projects.
- Step list tailored to the workflow stage (proposal, apply, archive), including strict validation commands.
- Pointers to `openspec show`, `openspec list`, and troubleshooting tips when validation fails.

## Testing Strategy
- Golden snapshots for generated files per tool (frontmatter + markers + body).
- Partial presence tests: if 1–2 files exist, `update` only refreshes those and does not create missing ones.
- Marker placement tests: ensure markers never appear inside frontmatter; cover missing/duplicated marker recovery behavior.
- Logging tests: `update` reports per-file updates for slash commands.


/*
 * Copyright 2024-2024 the original author or authors.
 */

package io.modelcontextprotocol.spec;

import java.util.List;

import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import io.modelcontextprotocol.json.TypeRef;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;

/**
 * Defines the asynchronous transport layer for the Model Context Protocol (MCP).
 *
 *

 * The McpTransport interface provides the foundation for implementing custom transport
 * mechanisms in the Model Context Protocol. It handles the bidirectional communication
 * between the client and server components, supporting asynchronous message exchange
 * using JSON-RPC format.
 *

 *
 *

 * Implementations of this interface are responsible for:
 *

 *

 *
Managing the lifecycle of the transport connection

 *
Handling incoming messages and errors from the server

 *
Sending outbound messages to the server

 *

 *
 *

 * The transport layer is designed to be protocol-agnostic, allowing for various
 * implementations such as WebSocket, HTTP, or custom protocols.
 *

 *
 * @author Christian Tzolov
 * @author Dariusz Jędrzejczyk
 */
public interface McpTransport {

	Logger logger = LoggerFactory.getLogger(McpTransport.class);

	/**
	 * Closes the transport connection and releases any associated resources.
	 *
	 *

	 * This method ensures proper cleanup of resources when the transport is no longer
	 * needed. It should handle the graceful shutdown of any active connections.
	 *

	 */
	default void close() {
		this.closeGracefully().subscribe(ignored -> {
		}, error -> {
			if (isPeerClosed(error)) {
				logger.debug("Error during asynchronous close", error);
			}
			else {
				logger.warn("Error during asynchronous close", error);
			}
		});
	}

	static boolean isPeerClosed(Throwable t) {
		for (Throwable c = t; c != null; c = c.getCause()) {
			if (c instanceof java.io.EOFException) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Closes the transport connection and releases any associated resources
	 * asynchronously.
	 * @return a {@link Mono<Void>} that completes when the connection has been closed.
	 */
	Mono<Void> closeGracefully();

	/**
	 * Sends a message to the peer asynchronously.
	 *
	 *

	 * This method handles the transmission of messages to the server in an asynchronous
	 * manner. Messages are sent in JSON-RPC format as specified by the MCP protocol.
	 *

	 * @param message the {@link JSONRPCMessage} to be sent to the server
	 * @return a {@link Mono<Void>} that completes when the message has been sent
	 */
	Mono<Void> sendMessage(JSONRPCMessage message);

	/**
	 * Unmarshals the given data into an object of the specified type.
	 * @param <T> the type of the object to unmarshal
	 * @param data the data to unmarshal
	 * @param typeRef the type reference for the object to unmarshal
	 * @return the unmarshalled object
	 */
	<T> T unmarshalFrom(Object data, TypeRef<T> typeRef);

	default List<String> protocolVersions() {
		return List.of(ProtocolVersions.MCP_2024_11_05, ProtocolVersions.MCP_2025_03_26,
				ProtocolVersions.MCP_2025_06_18, ProtocolVersions.MCP_2025_11_25);
	}

}


/*
===============================================================================
Stored Procedure: Load Silver Layer (Bronze -> Silver)
===============================================================================
Script Purpose:
    This stored procedure performs the ETL (Extract, Transform, Load) process to
    populate the 'silver' schema tables from the 'bronze' schema.
	Actions Performed:
		- Truncates Silver tables.
		- Inserts transformed and cleansed data from Bronze into Silver tables.

Parameters:
    None.
	  This stored procedure does not accept any parameters or return any values.

Usage Example:
    EXEC Silver.load_silver;
===============================================================================
*/

CREATE OR ALTER PROCEDURE silver.load_silver AS
BEGIN
    DECLARE @start_time DATETIME, @end_time DATETIME, @batch_start_time DATETIME, @batch_end_time DATETIME;
    BEGIN TRY
        SET @batch_start_time = GETDATE();
        PRINT '================================================';
        PRINT 'Loading Silver Layer';
        PRINT '================================================';

		PRINT '------------------------------------------------';
		PRINT 'Loading CRM Tables';
		PRINT '------------------------------------------------';

		-- Loading silver.crm_cust_info
        SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.crm_cust_info';
		TRUNCATE TABLE silver.crm_cust_info;
		PRINT '>> Inserting Data Into: silver.crm_cust_info';
		INSERT INTO silver.crm_cust_info (
			cst_id,
			cst_key,
			cst_firstname,
			cst_lastname,
			cst_marital_status,
			cst_gndr,
			cst_create_date
		)
		SELECT
			cst_id,
			cst_key,
			TRIM(cst_firstname) AS cst_firstname,
			TRIM(cst_lastname) AS cst_lastname,
			CASE
				WHEN UPPER(TRIM(cst_marital_status)) = 'S' THEN 'Single'
				WHEN UPPER(TRIM(cst_marital_status)) = 'M' THEN 'Married'
				ELSE 'n/a'
			END AS cst_marital_status, -- Normalize marital status values to readable format
			CASE
				WHEN UPPER(TRIM(cst_gndr)) = 'F' THEN 'Female'
				WHEN UPPER(TRIM(cst_gndr)) = 'M' THEN 'Male'
				ELSE 'n/a'
			END AS cst_gndr, -- Normalize gender values to readable format
			cst_create_date
		FROM (
			SELECT
				*,
				ROW_NUMBER() OVER (PARTITION BY cst_id ORDER BY cst_create_date DESC) AS flag_last
			FROM bronze.crm_cust_info
			WHERE cst_id IS NOT NULL
		) t
		WHERE flag_last = 1; -- Select the most recent record per customer
		SET @end_time = GETDATE();
        PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

		-- Loading silver.crm_prd_info
        SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.crm_prd_info';
		TRUNCATE TABLE silver.crm_prd_info;
		PRINT '>> Inserting Data Into: silver.crm_prd_info';
		INSERT INTO silver.crm_prd_info (
			prd_id,
			cat_id,
			prd_key,
			prd_nm,
			prd_cost,
			prd_line,
			prd_start_dt,
			prd_end_dt
		)
		SELECT
			prd_id,
			REPLACE(SUBSTRING(prd_key, 1, 5), '-', '_') AS cat_id, -- Extract category ID
			SUBSTRING(prd_key, 7, LEN(prd_key)) AS prd_key,        -- Extract product key
			prd_nm,
			ISNULL(prd_cost, 0) AS prd_cost,
			CASE
				WHEN UPPER(TRIM(prd_line)) = 'M' THEN 'Mountain'
				WHEN UPPER(TRIM(prd_line)) = 'R' THEN 'Road'
				WHEN UPPER(TRIM(prd_line)) = 'S' THEN 'Other Sales'
				WHEN UPPER(TRIM(prd_line)) = 'T' THEN 'Touring'
				ELSE 'n/a'
			END AS prd_line, -- Map product line codes to descriptive values
			CAST(prd_start_dt AS DATE) AS prd_start_dt,
			CAST(
				LEAD(prd_start_dt) OVER (PARTITION BY prd_key ORDER BY prd_start_dt) - 1
				AS DATE
			) AS prd_end_dt -- Calculate end date as one day before the next start date
		FROM bronze.crm_prd_info;
        SET @end_time = GETDATE();
        PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

        -- Loading crm_sales_details
        SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.crm_sales_details';
		TRUNCATE TABLE silver.crm_sales_details;
		PRINT '>> Inserting Data Into: silver.crm_sales_details';
		INSERT INTO silver.crm_sales_details (
			sls_ord_num,
			sls_prd_key,
			sls_cust_id,
			sls_order_dt,
			sls_ship_dt,
			sls_due_dt,
			sls_sales,
			sls_quantity,
			sls_price
		)
		SELECT
			sls_ord_num,
			sls_prd_key,
			sls_cust_id,
			CASE
				WHEN sls_order_dt = 0 OR LEN(sls_order_dt) != 8 THEN NULL
				ELSE CAST(CAST(sls_order_dt AS VARCHAR) AS DATE)
			END AS sls_order_dt,
			CASE
				WHEN sls_ship_dt = 0 OR LEN(sls_ship_dt) != 8 THEN NULL
				ELSE CAST(CAST(sls_ship_dt AS VARCHAR) AS DATE)
			END AS sls_ship_dt,
			CASE
				WHEN sls_due_dt = 0 OR LEN(sls_due_dt) != 8 THEN NULL
				ELSE CAST(CAST(sls_due_dt AS VARCHAR) AS DATE)
			END AS sls_due_dt,
			CASE
				WHEN sls_sales IS NULL OR sls_sales <= 0 OR sls_sales != sls_quantity * ABS(sls_price)
					THEN sls_quantity * ABS(sls_price)
				ELSE sls_sales
			END AS sls_sales, -- Recalculate sales if original value is missing or incorrect
			sls_quantity,
			CASE
				WHEN sls_price IS NULL OR sls_price <= 0
					THEN sls_sales / NULLIF(sls_quantity, 0)
				ELSE sls_price  -- Derive price if original value is invalid
			END AS sls_price
		FROM bronze.crm_sales_details;
        SET @end_time = GETDATE();
        PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

        -- Loading erp_cust_az12
        SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.erp_cust_az12';
		TRUNCATE TABLE silver.erp_cust_az12;
		PRINT '>> Inserting Data Into: silver.erp_cust_az12';
		INSERT INTO silver.erp_cust_az12 (
			cid,
			bdate,
			gen
		)
		SELECT
			CASE
				WHEN cid LIKE 'NAS%' THEN SUBSTRING(cid, 4, LEN(cid)) -- Remove 'NAS' prefix if present
				ELSE cid
			END AS cid,
			CASE
				WHEN bdate > GETDATE() THEN NULL
				ELSE bdate
			END AS bdate, -- Set future birthdates to NULL
			CASE
				WHEN UPPER(TRIM(gen)) IN ('F', 'FEMALE') THEN 'Female'
				WHEN UPPER(TRIM(gen)) IN ('M', 'MALE') THEN 'Male'
				ELSE 'n/a'
			END AS gen -- Normalize gender values and handle unknown cases
		FROM bronze.erp_cust_az12;
	    SET @end_time = GETDATE();
        PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

		PRINT '------------------------------------------------';
		PRINT 'Loading ERP Tables';
		PRINT '------------------------------------------------';

        -- Loading erp_loc_a101
        SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.erp_loc_a101';
		TRUNCATE TABLE silver.erp_loc_a101;
		PRINT '>> Inserting Data Into: silver.erp_loc_a101';
		INSERT INTO silver.erp_loc_a101 (
			cid,
			cntry
		)
		SELECT
			REPLACE(cid, '-', '') AS cid,
			CASE
				WHEN TRIM(cntry) = 'DE' THEN 'Germany'
				WHEN TRIM(cntry) IN ('US', 'USA') THEN 'United States'
				WHEN TRIM(cntry) = '' OR cntry IS NULL THEN 'n/a'
				ELSE TRIM(cntry)
			END AS cntry -- Normalize and Handle missing or blank country codes
		FROM bronze.erp_loc_a101;
	    SET @end_time = GETDATE();
        PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

		-- Loading erp_px_cat_g1v2
		SET @start_time = GETDATE();
		PRINT '>> Truncating Table: silver.erp_px_cat_g1v2';
		TRUNCATE TABLE silver.erp_px_cat_g1v2;
		PRINT '>> Inserting Data Into: silver.erp_px_cat_g1v2';
		INSERT INTO silver.erp_px_cat_g1v2 (
			id,
			cat,
			subcat,
			maintenance
		)
		SELECT
			id,
			cat,
			subcat,
			maintenance
		FROM bronze.erp_px_cat_g1v2;
		SET @end_time = GETDATE();
		PRINT '>> Load Duration: ' + CAST(DATEDIFF(SECOND, @start_time, @end_time) AS NVARCHAR) + ' seconds';
        PRINT '>> -------------';

		SET @batch_end_time = GETDATE();
		PRINT '=========================================='
		PRINT 'Loading Silver Layer is Completed';
        PRINT '   - Total Load Duration: ' + CAST(DATEDIFF(SECOND, @batch_start_time, @batch_end_time) AS NVARCHAR) + ' seconds';
		PRINT '=========================================='

	END TRY
	BEGIN CATCH
		PRINT '=========================================='
		PRINT 'ERROR OCCURED DURING LOADING BRONZE LAYER'
		PRINT 'Error Message' + ERROR_MESSAGE();
		PRINT 'Error Message' + CAST (ERROR_NUMBER() AS NVARCHAR);
		PRINT 'Error Message' + CAST (ERROR_STATE() AS NVARCHAR);
		PRINT '=========================================='
	END CATCH
END


import {
  CompletionGenerator,
  CommandDefinition,
  FlagDefinition,
  PositionalDefinition,
} from '../types.js';
import { POWERSHELL_DYNAMIC_HELPERS } from '../templates/powershell-templates.js';

/**
 * Generates PowerShell completion scripts for the OpenSpec CLI.
 * Uses Register-ArgumentCompleter for command completion.
 */
export class PowerShellGenerator implements CompletionGenerator {
  readonly shell = 'powershell' as const;

  private stripTrailingCommaFromLastLine(lines: string[]): void {
    if (lines.length === 0) return;
    lines[lines.length - 1] = lines[lines.length - 1].replace(/,\s*$/, '');
  }

  /**
   * Generate a PowerShell completion script
   *
   * @param commands - Command definitions to generate completions for
   * @returns PowerShell completion script as a string
   */
  generate(commands: CommandDefinition[]): string {
    // Build top-level commands using push() for loop clarity
    const commandLines: string[] = [];
    for (const cmd of commands) {
      commandLines.push(`            @{Name="${cmd.name}"; Description="${this.escapeDescription(cmd.description)}"},`);
    }
    this.stripTrailingCommaFromLastLine(commandLines);
    const topLevelCommands = commandLines.join('\n');

    // Build command cases using push() for loop clarity
    const commandCaseLines: string[] = [];
    for (const cmd of commands) {
      commandCaseLines.push(`        "${cmd.name}" {`);
      commandCaseLines.push(...this.generateCommandCase(cmd, '            '));
      commandCaseLines.push('        }');
    }
    const commandCases = commandCaseLines.join('\n');

    // Dynamic completion helpers from template
    const helpers = POWERSHELL_DYNAMIC_HELPERS;

    // Assemble final script with template literal
    return `# PowerShell completion script for OpenSpec CLI
# Auto-generated - do not edit manually

${helpers}
$openspecCompleter = {
    param($wordToComplete, $commandAst, $cursorPosition)

    $tokens = $commandAst.ToString() -split "\\s+"
    $commandCount = ($tokens | Measure-Object).Count

    # Top-level commands
    if ($commandCount -eq 1 -or ($commandCount -eq 2 -and $wordToComplete)) {
        $commands = @(
${topLevelCommands}
        )
        $commands | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
            [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, "ParameterValue", $_.Description)
        }
        return
    }

    $command = $tokens[1]

    switch ($command) {
${commandCases}
    }
}

Register-ArgumentCompleter -CommandName openspec -ScriptBlock $openspecCompleter
`;
  }

  /**
   * Generate completion case for a command
   */
  private generateCommandCase(cmd: CommandDefinition, indent: string): string[] {
    const lines: string[] = [];

    if (cmd.subcommands && cmd.subcommands.length > 0) {
      // First, check if user is typing a flag for the parent command
      if (cmd.flags.length > 0) {
        lines.push(`${indent}if ($wordToComplete -like "-*") {`);
        lines.push(`${indent}    $flags = @(`);
        for (const flag of cmd.flags) {
          const longFlag = `--${flag.name}`;
          const shortFlag = flag.short ? `-${flag.short}` : undefined;
          if (shortFlag) {
            lines.push(`${indent}        @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
            lines.push(`${indent}        @{Name="${shortFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
          } else {
            lines.push(`${indent}        @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
          }
        }
        this.stripTrailingCommaFromLastLine(lines);
        lines.push(`${indent}    )`);
        lines.push(`${indent}    $flags | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, "ParameterName", $_.Description)`);
        lines.push(`${indent}    }`);
        lines.push(`${indent}    return`);
        lines.push(`${indent}}`);
        lines.push('');
      }

      // Handle subcommands
      lines.push(`${indent}if ($commandCount -eq 2 -or ($commandCount -eq 3 -and $wordToComplete)) {`);
      lines.push(`${indent}    $subcommands = @(`);
      for (const subcmd of cmd.subcommands) {
        lines.push(`${indent}        @{Name="${subcmd.name}"; Description="${this.escapeDescription(subcmd.description)}"},`);
      }
      this.stripTrailingCommaFromLastLine(lines);
      lines.push(`${indent}    )`);
      lines.push(`${indent}    $subcommands | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {`);
      lines.push(`${indent}        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, "ParameterValue", $_.Description)`);
      lines.push(`${indent}    }`);
      lines.push(`${indent}    return`);
      lines.push(`${indent}}`);
      lines.push('');
      lines.push(`${indent}$subcommand = if ($commandCount -gt 2) { $tokens[2] } else { "" }`);
      lines.push(`${indent}switch ($subcommand) {`);

      for (const subcmd of cmd.subcommands) {
        lines.push(`${indent}    "${subcmd.name}" {`);
        lines.push(...this.generateArgumentCompletion(subcmd, indent + '        ', 3));
        lines.push(`${indent}    }`);
      }

      lines.push(`${indent}}`);
    } else {
      // No subcommands
      lines.push(...this.generateArgumentCompletion(cmd, indent, 2));
    }

    return lines;
  }

  /**
   * Generate argument completion (flags and positional)
   */
  private generateArgumentCompletion(
    cmd: CommandDefinition,
    indent: string,
    firstPositionalTokenIndex: number
  ): string[] {
    const lines: string[] = [];

    // Flag completion
    if (cmd.flags.length > 0) {
      lines.push(`${indent}if ($wordToComplete -like "-*") {`);
      lines.push(`${indent}    $flags = @(`);
      for (const flag of cmd.flags) {
        const longFlag = `--${flag.name}`;
        const shortFlag = flag.short ? `-${flag.short}` : undefined;
        if (shortFlag) {
          lines.push(`${indent}        @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
          lines.push(`${indent}        @{Name="${shortFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
        } else {
          lines.push(`${indent}        @{Name="${longFlag}"; Description="${this.escapeDescription(flag.description)}"},`);
        }
      }
      this.stripTrailingCommaFromLastLine(lines);
      lines.push(`${indent}    )`);
      lines.push(`${indent}    $flags | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {`);
      lines.push(`${indent}        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, "ParameterName", $_.Description)`);
      lines.push(`${indent}    }`);
      lines.push(`${indent}    return`);
      lines.push(`${indent}}`);
      lines.push('');
    }

    // Positional completion
    if (cmd.positionals && cmd.positionals.length > 0) {
      lines.push(...this.generateIndexedPositionalCompletion(
        cmd.positionals,
        cmd.flags,
        firstPositionalTokenIndex,
        indent
      ));
    } else if (cmd.acceptsPositional) {
      lines.push(...this.generatePositionalCompletion(cmd.positionalType, indent));
    }

    return lines;
  }

  private generateIndexedPositionalCompletion(
    positionals: PositionalDefinition[],
    flags: FlagDefinition[],
    firstPositionalTokenIndex: number,
    indent: string
  ): string[] {
    const caseLines: string[] = [];
    for (const [index, positional] of positionals.entries()) {
      const completion = this.generatePositionalCompletion(positional.type, indent + '    ');
      if (completion.length === 0) continue;
      caseLines.push(`${indent}    ${index} {`);
      caseLines.push(...completion);
      caseLines.push(`${indent}    }`);
    }

    // A switch with no clauses is a PowerShell parse error, so when no
    // positional produces completions skip the whole block (it would only
    // feed the empty switch anyway).
    if (caseLines.length === 0) return [];

    const lines: string[] = [];
    const valueFlags = this.generateValueFlags(flags);

    if (valueFlags.length > 0) {
      const flagList = valueFlags.map((flag) => `"${flag}"`).join(', ');
      lines.push(`${indent}if (@(${flagList}) -contains $tokens[$commandCount - 2]) { return }`);
      lines.push('');
    }

    lines.push(`${indent}$positionalIndex = 0`);
    lines.push(`${indent}$skipNext = $false`);
    lines.push(`${indent}for ($i = ${firstPositionalTokenIndex}; $i -lt ($commandCount - 1); $i++) {`);
    lines.push(`${indent}    if ($skipNext) {`);
    lines.push(`${indent}        $skipNext = $false`);
    lines.push(`${indent}        continue`);
    lines.push(`${indent}    }`);
    lines.push(`${indent}    $token = $tokens[$i]`);

    if (valueFlags.length > 0) {
      const flagList = valueFlags.map((flag) => `"${flag}"`).join(', ');
      lines.push(`${indent}    if (@(${flagList}) -contains $token) {`);
      lines.push(`${indent}        $skipNext = $true`);
      lines.push(`${indent}        continue`);
      lines.push(`${indent}    }`);
      lines.push(`${indent}    if ($token -match "^(${valueFlags.map((flag) => this.escapeRegex(flag)).join('|')})=.*") { continue }`);
    }

    lines.push(`${indent}    if ($token -like "-*") { continue }`);
    lines.push(`${indent}    $positionalIndex++`);
    lines.push(`${indent}}`);
    lines.push('');
    lines.push(`${indent}switch ($positionalIndex) {`);
    lines.push(...caseLines);
    lines.push(`${indent}}`);

    return lines;
  }

  private generateValueFlags(flags: FlagDefinition[]): string[] {
    return flags
      .filter((flag) => flag.takesValue)
      .flatMap((flag) => [
        `--${flag.name}`,
        ...(flag.short ? [`-${flag.short}`] : []),
      ]);
  }

  private escapeRegex(value: string): string {
    return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  /**
   * Generate positional argument completion
   */
  private generatePositionalCompletion(positionalType: string | undefined, indent: string): string[] {
    const lines: string[] = [];

    switch (positionalType) {
      case 'change-id':
        lines.push(`${indent}Get-OpenSpecChanges | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}    [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", "Change: $_")`);
        lines.push(`${indent}}`);
        break;
      case 'spec-id':
        lines.push(`${indent}Get-OpenSpecSpecs | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}    [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", "Spec: $_")`);
        lines.push(`${indent}}`);
        break;
      case 'change-or-spec-id':
        lines.push(`${indent}$items = @(Get-OpenSpecChanges) + @(Get-OpenSpecSpecs)`);
        lines.push(`${indent}$items | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}    [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_)`);
        lines.push(`${indent}}`);
        break;
      case 'schema-name':
        lines.push(`${indent}Get-OpenSpecSchemas | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}    [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", "Schema: $_")`);
        lines.push(`${indent}}`);
        break;
      case 'shell':
        lines.push(`${indent}$shells = @("zsh", "bash", "fish", "powershell")`);
        lines.push(`${indent}$shells | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {`);
        lines.push(`${indent}    [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", "Shell: $_")`);
        lines.push(`${indent}}`);
        break;
      case 'path':
        // PowerShell handles file path completion automatically
        break;
    }

    return lines;
  }

  /**
   * Escape description text for PowerShell
   */
  private escapeDescription(description: string): string {
    return description
      .replace(/`/g, '``')     // Backticks (escape sequences)
      .replace(/\$/g, '`$')    // Dollar signs (prevents $())
      .replace(/"/g, '""');    // Double quotes
  }
}


import * as path from 'node:path';
import { formatCookieHeader } from '@jackwener/opencli/download';
import { downloadMedia } from '@jackwener/opencli/download/media-download';
import { cli, Strategy } from '@jackwener/opencli/registry';
import { cleanText } from './shared.js';
import { extractAssetsForInput } from './assets.js';
function extFromUrl(url, fallback) {
    try {
        const ext = path.extname(new URL(url).pathname).toLowerCase();
        if (ext && ext.length <= 8)
            return ext;
    }
    catch {
        // ignore
    }
    return fallback;
}
function toDownloadItems(offerId, assets) {
    const items = [];
    const pushImages = (urls, prefix) => {
        urls.forEach((url, index) => {
            items.push({
                type: 'image',
                url,
                filename: `${offerId}_${prefix}_${String(index + 1).padStart(2, '0')}${extFromUrl(url, '.jpg')}`,
            });
        });
    };
    pushImages(assets.main_images, 'main');
    pushImages(assets.sku_images, 'sku');
    pushImages(assets.detail_images, 'detail');
    pushImages(assets.other_images, 'other');
    assets.videos.forEach((url, index) => {
        items.push({
            type: 'video',
            url,
            filename: `${offerId}_video_${String(index + 1).padStart(2, '0')}${extFromUrl(url, '.mp4')}`,
        });
    });
    return items;
}
cli({
    site: '1688',
    name: 'download',
    access: 'read',
    description: '批量下载 1688 商品页可提取的图片和视频素材',
    domain: 'www.1688.com',
    strategy: Strategy.COOKIE,
    args: [
        {
            name: 'input',
            required: true,
            positional: true,
            help: '1688 商品 URL 或 offer ID（如 887904326744）',
        },
        { name: 'output', default: './1688-downloads', help: '输出目录' },
    ],
    columns: ['index', 'type', 'status', 'size'],
    func: async (page, kwargs) => {
        const assets = await extractAssetsForInput(page, String(kwargs.input ?? ''));
        const offerId = cleanText(assets.offer_id) || '1688';
        const items = toDownloadItems(offerId, assets);
        const browserCookies = await page.getCookies({ domain: '1688.com' });
        return downloadMedia(items, {
            output: String(kwargs.output || './1688-downloads'),
            subdir: offerId,
            cookies: formatCookieHeader(browserCookies),
            browserCookies,
            filenamePrefix: offerId,
            timeout: 60000,
        });
    },
});
export const __test__ = {
    extFromUrl,
    toDownloadItems,
};