use llm_extract::{
    extract, extract_all, extract_json, extract_json_all, extract_strict, extract_verbose, Extract,
    ExtractError, Extractor, RepairMode,
};
use serde::Deserialize;

// ---------------------------------------------------------------------------
// Layer 1 tests (Value extraction)
// ---------------------------------------------------------------------------

#[test]
fn bare_json_object() {
    let v = extract_json(r#"{"a": 1}"#).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn bare_json_array() {
    let v = extract_json("[1, 2, 3]").unwrap();
    assert_eq!(v[0], 1);
    assert_eq!(v[2], 3);
}

#[test]
fn fenced_json() {
    let text = "```json\n{\"status\": \"ok\", \"count\": 42}\n```";
    let v = extract_json(text).unwrap();
    assert_eq!(v["status"], "ok");
    assert_eq!(v["count"], 42);
}

#[test]
fn fenced_no_lang() {
    let text = "```\n{\"a\": 1}\n```";
    let v = extract_json(text).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn json_with_prose_before() {
    let text = "Here's the data: {\"a\": 1}";
    let v = extract_json(text).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn json_with_prose_after() {
    let text = "{\"a\": 1}\n\nExplanation: this is the result.";
    let v = extract_json(text).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn json_with_prose_both() {
    let text = "Sure! {\"a\": 1} Hope that helps!";
    let v = extract_json(text).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn trailing_comma_object() {
    let v = extract_json(r#"{"a": 1,}"#).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn trailing_comma_array() {
    let v = extract_json("[1, 2,]").unwrap();
    assert_eq!(v[0], 1);
    assert_eq!(v[1], 2);
}

#[test]
fn nested_trailing_commas() {
    let v = extract_json(r#"{"a": [1,], "b": {"c": 2,},}"#).unwrap();
    assert_eq!(v["a"][0], 1);
    assert_eq!(v["b"]["c"], 2);
}

#[test]
fn unclosed_string() {
    let v = extract_json(r#"{"a": "test"#).unwrap();
    assert_eq!(v["a"], "test");
}

#[test]
fn unclosed_bracket() {
    let v = extract_json(r#"{"a": 1"#).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn multiple_json_entities() {
    let text = r#"{"a": 1} some text {"b": 2}"#;
    let v = extract_json(text).unwrap();
    assert_eq!(v["a"], 1);
}

#[test]
fn empty_array() {
    let v = extract_json("[]").unwrap();
    assert!(v.as_array().unwrap().is_empty());
}

#[test]
fn empty_object() {
    let v = extract_json("{}").unwrap();
    assert!(v.as_object().unwrap().is_empty());
}

#[test]
fn no_json_found() {
    // jsonrepair may parse plain text as a JSON string, so use empty input
    let err = extract_json("").unwrap_err();
    assert!(matches!(
        err,
        ExtractError::NotFound | ExtractError::Parse { .. }
    ));
}

#[test]
fn commas_in_strings_preserved() {
    let v = extract_json(r#"{"msg": "hello, world"}"#).unwrap();
    assert_eq!(v["msg"], "hello, world");
}

// ---------------------------------------------------------------------------
// Layer 2 tests (typed extraction + repair)
// ---------------------------------------------------------------------------

#[derive(Debug, PartialEq, Deserialize, Extract)]
struct ApiResult {
    status: String,
    count: u32,
}

#[test]
fn deserialize_to_struct() {
    let text = "```json\n{\"status\": \"ok\", \"count\": 42}\n```";
    let result: ApiResult = extract(text).unwrap();
    assert_eq!(
        result,
        ApiResult {
            status: "ok".to_string(),
            count: 42
        }
    );
}

#[test]
fn strict_mode_no_repair() {
    let text = r#"{"status": "ok", "count": 42}"#;
    let result: ApiResult = extract_strict(text).unwrap();
    assert_eq!(result.status, "ok");
}

#[derive(Debug, PartialEq, Deserialize, Extract)]
struct Task {
    target: String,
    priority: u32,
}

#[test]
fn repair_field_name_typo() {
    // "taget" should be repaired to "target"
    let text = r#"{"taget": "main.rs", "priority": 1}"#;
    let result: Task = extract(text).unwrap();
    assert_eq!(result.target, "main.rs");
    assert_eq!(result.priority, 1);
}

#[derive(Debug, PartialEq, Deserialize, Extract)]
#[extract(tag = "type")]
enum ActionKind {
    AddDerive,
    RemoveField,
    RenameVariable,
}

#[derive(Debug, PartialEq, Deserialize, Extract)]
struct Action {
    #[serde(rename = "type")]
    action_type: String,
    target: String,
}

#[test]
fn extract_with_serde_rename() {
    let text = r#"{"type": "AddDerive", "target": "User"}"#;
    let result: Action = extract(text).unwrap();
    assert_eq!(result.action_type, "AddDerive");
    assert_eq!(result.target, "User");
}

#[test]
fn repair_multiple_field_typos() {
    // "taget" → "target", struct repair should handle it
    let text = r#"{"taget": "User", "priorty": 5}"#;
    let result: Task = extract(text).unwrap();
    assert_eq!(result.target, "User");
    assert_eq!(result.priority, 5);
}

#[test]
fn strict_mode_rejects_typo() {
    let text = r#"{"taget": "main.rs", "priority": 1}"#;
    let err = extract_strict::<Task>(text).unwrap_err();
    assert!(matches!(err, ExtractError::Parse { .. }));
}

// ---------------------------------------------------------------------------
// Extractor builder tests
// ---------------------------------------------------------------------------

#[test]
fn custom_extractor() {
    let extractor = Extractor::builder()
        .max_input_len(512 * 1024)
        .repair(RepairMode::default())
        .build();

    let text = r#"{"status": "ok", "count": 42}"#;
    let result: ApiResult = extractor.extract(text).unwrap();
    assert_eq!(result.status, "ok");
}

#[test]
fn strict_extractor() {
    let extractor = Extractor::builder().repair(RepairMode::Strict).build();

    let text = r#"{"status": "ok", "count": 42}"#;
    let result: ApiResult = extractor.extract(text).unwrap();
    assert_eq!(result.status, "ok");
}

// ---------------------------------------------------------------------------
// derive(Extract) schema tests
// ---------------------------------------------------------------------------

#[test]
fn derive_generates_struct_schema() {
    let schema = Task::repair_schema();
    let fields = schema.field_names();
    assert!(fields.contains(&"target"));
    assert!(fields.contains(&"priority"));
}

#[test]
fn derive_generates_enum_schema() {
    let schema = ActionKind::repair_schema();
    let (tag, variants) = schema.tag_info().expect("should be tagged enum");
    assert_eq!(tag, "type");
    assert!(variants.contains(&"AddDerive"));
    assert!(variants.contains(&"RemoveField"));
    assert!(variants.contains(&"RenameVariable"));
}

#[test]
fn derive_respects_serde_rename() {
    let schema = Action::repair_schema();
    let fields = schema.field_names();
    // "type" from #[serde(rename = "type")], not "action_type"
    assert!(fields.contains(&"type"));
    assert!(fields.contains(&"target"));
}

// ---------------------------------------------------------------------------
// Complex LLM output scenarios
// ---------------------------------------------------------------------------

#[test]
fn full_llm_response_with_explanation() {
    let text = r#"Based on my analysis, here's the result:

```json
{
  "status": "complete",
  "count": 7
}
```

The count of 7 reflects the total number of items found."#;

    let result: ApiResult = extract(text).unwrap();
    assert_eq!(result.status, "complete");
    assert_eq!(result.count, 7);
}

#[test]
fn llm_response_with_trailing_commas_and_prose() {
    let text = r#"Sure! Here's the task:
{"taget": "lib.rs", "priority": 3,}
Let me know if you need changes."#;

    let result: Task = extract(text).unwrap();
    assert_eq!(result.target, "lib.rs");
    assert_eq!(result.priority, 3);
}

// ---------------------------------------------------------------------------
// jsonrepair-powered syntax repair tests
// ---------------------------------------------------------------------------

#[test]
fn single_quotes() {
    let v = extract_json("{'status': 'ok', 'count': 42}").unwrap();
    assert_eq!(v["status"], "ok");
    assert_eq!(v["count"], 42);
}

#[test]
fn unquoted_keys() {
    let v = extract_json("{status: \"ok\", count: 42}").unwrap();
    assert_eq!(v["status"], "ok");
    assert_eq!(v["count"], 42);
}

#[test]
fn python_true_false_none() {
    let v = extract_json(r#"{"active": True, "deleted": False, "data": None}"#).unwrap();
    assert_eq!(v["active"], true);
    assert_eq!(v["deleted"], false);
    assert!(v["data"].is_null());
}

#[test]
fn line_comments() {
    let text = r#"{
        // user info
        "name": "Alice",
        "age": 30 // years
    }"#;
    let v = extract_json(text).unwrap();
    assert_eq!(v["name"], "Alice");
    assert_eq!(v["age"], 30);
}

#[test]
fn block_comments() {
    let text = r#"{
        /* metadata */
        "version": 1,
        "stable": true
    }"#;
    let v = extract_json(text).unwrap();
    assert_eq!(v["version"], 1);
    assert_eq!(v["stable"], true);
}

#[test]
fn single_quotes_with_prose_and_fence() {
    let text = "Here's the result:\n```json\n{'name': 'Bob', 'score': 95}\n```\nDone!";
    let v = extract_json(text).unwrap();
    assert_eq!(v["name"], "Bob");
    assert_eq!(v["score"], 95);
}

#[test]
fn python_style_dict_in_fence() {
    let text = "```\n{'active': True, 'count': None, 'items': [1, 2,]}\n```";
    let v = extract_json(text).unwrap();
    assert_eq!(v["active"], true);
    assert!(v["count"].is_null());
    assert_eq!(v["items"][0], 1);
}

#[test]
fn combined_syntax_errors() {
    // Single quotes + unquoted keys + trailing comma + Python keyword
    let text = "{name: 'Alice', active: True, tags: ['admin', 'user',],}";
    let v = extract_json(text).unwrap();
    assert_eq!(v["name"], "Alice");
    assert_eq!(v["active"], true);
    assert_eq!(v["tags"][0], "admin");
}

#[test]
fn typed_extract_with_single_quotes() {
    let text = "{'status': 'ok', 'count': 42}";
    let result: ApiResult = extract(text).unwrap();
    assert_eq!(result.status, "ok");
    assert_eq!(result.count, 42);
}

#[test]
fn typed_extract_with_python_keywords() {
    #[derive(Debug, PartialEq, Deserialize, Extract)]
    struct Config {
        enabled: bool,
        retries: u32,
    }
    let text = "{'enabled': True, 'retries': 3}";
    let result: Config = extract(text).unwrap();
    assert!(result.enabled);
    assert_eq!(result.retries, 3);
}

// ---------------------------------------------------------------------------
// <think> block removal tests
// ---------------------------------------------------------------------------

#[test]
fn think_block_with_json() {
    let text = "<think>Let me analyze this request...</think>\n{\"status\": \"ok\", \"count\": 42}";
    let v = extract_json(text).unwrap();
    assert_eq!(v["status"], "ok");
    assert_eq!(v["count"], 42);
}

#[test]
fn think_block_with_fence_and_prose() {
    let text = r#"<think>
The user wants structured data. Let me format it as JSON.
</think>

Here's the result:

```json
{"status": "complete", "count": 7}
```

The count reflects the total items found."#;

    let result: ApiResult = extract(text).unwrap();
    assert_eq!(result.status, "complete");
    assert_eq!(result.count, 7);
}

#[test]
fn think_block_with_typo_repair() {
    let text =
        "<think>I should return a Task object.</think>\n{\"taget\": \"main.rs\", \"priority\": 1}";
    let result: Task = extract(text).unwrap();
    assert_eq!(result.target, "main.rs");
    assert_eq!(result.priority, 1);
}

// ---------------------------------------------------------------------------
// extract_all / extract_json_all tests
// ---------------------------------------------------------------------------

#[test]
fn extract_json_all_multiple_objects() {
    let text = r#"First result: {"status": "ok", "count": 1}
Second result: {"status": "done", "count": 2}"#;
    let values = extract_json_all(text).unwrap();
    assert_eq!(values.len(), 2);
    assert_eq!(values[0]["status"], "ok");
    assert_eq!(values[1]["status"], "done");
}

#[test]
fn extract_json_all_single_object() {
    let text = r#"{"status": "ok", "count": 42}"#;
    let values = extract_json_all(text).unwrap();
    assert_eq!(values.len(), 1);
}

#[test]
fn extract_json_all_no_json() {
    let err = extract_json_all("no json here").unwrap_err();
    assert!(matches!(err, ExtractError::NotFound));
}

#[test]
fn extract_all_typed_multiple() {
    let text = r#"Task 1: {"target": "main.rs", "priority": 1}
Task 2: {"target": "lib.rs", "priority": 2}"#;
    let tasks: Vec<Task> = extract_all(text).unwrap();
    assert_eq!(tasks.len(), 2);
    assert_eq!(tasks[0].target, "main.rs");
    assert_eq!(tasks[1].target, "lib.rs");
}

#[test]
fn extract_all_with_repair() {
    let text = r#"{"taget": "main.rs", "priority": 1} {"taget": "lib.rs", "priority": 2}"#;
    let tasks: Vec<Task> = extract_all(text).unwrap();
    assert_eq!(tasks.len(), 2);
    assert_eq!(tasks[0].target, "main.rs");
    assert_eq!(tasks[1].target, "lib.rs");
}

#[test]
fn extract_json_all_with_think_block() {
    let text = r#"<think>Multiple results needed</think>
{"a": 1} {"b": 2}"#;
    let values = extract_json_all(text).unwrap();
    assert_eq!(values.len(), 2);
}

// ---------------------------------------------------------------------------
// Nested recursive repair tests
// ---------------------------------------------------------------------------

#[derive(Debug, PartialEq, Deserialize, Extract)]
struct Inner {
    description: String,
    value: u32,
}

#[derive(Debug, PartialEq, Deserialize, Extract)]
struct Outer {
    name: String,
    #[extract(nested = true)]
    details: Inner,
}

#[test]
fn nested_repair_inner_field_typo() {
    // "descrption" → "description" in nested struct
    let text = r#"{"name": "test", "details": {"descrption": "hello", "value": 42}}"#;
    let result: Outer = extract(text).unwrap();
    assert_eq!(result.name, "test");
    assert_eq!(result.details.description, "hello");
    assert_eq!(result.details.value, 42);
}

#[test]
fn nested_repair_both_levels_typo() {
    // "nme" → "name" at outer, "descrption" → "description" at inner
    let text = r#"{"nme": "test", "details": {"descrption": "hello", "value": 42}}"#;
    let result: Outer = extract(text).unwrap();
    assert_eq!(result.name, "test");
    assert_eq!(result.details.description, "hello");
}

#[test]
fn nested_no_repair_without_attribute() {
    // Without #[extract(nested)] the inner struct is NOT repaired
    #[derive(Debug, PartialEq, Deserialize, Extract)]
    struct OuterNoNested {
        name: String,
        details: Inner,
    }
    // Inner typo should NOT be repaired
    let text = r#"{"name": "test", "details": {"descrption": "hello", "value": 42}}"#;
    let result = extract::<OuterNoNested>(text);
    // Should fail because inner field typo is not repaired
    assert!(result.is_err());
}

// ---------------------------------------------------------------------------
// extract_verbose tests (corrections returned on success)
// ---------------------------------------------------------------------------

#[test]
fn verbose_no_corrections_needed() {
    let text = r#"{"status": "ok", "count": 42}"#;
    let result = extract_verbose::<ApiResult>(text).unwrap();
    assert_eq!(result.value.status, "ok");
    assert!(result.corrections.is_empty());
}

#[test]
fn verbose_with_field_corrections() {
    let text = r#"{"taget": "main.rs", "priority": 1}"#;
    let result = extract_verbose::<Task>(text).unwrap();
    assert_eq!(result.value.target, "main.rs");
    assert!(!result.corrections.is_empty());
    assert!(result.corrections.iter().any(|c| c.corrected == "target"));
}

#[test]
fn verbose_via_extractor() {
    let extractor = Extractor::builder().build();
    let text = r#"{"taget": "lib.rs", "priority": 2}"#;
    let result = extractor.extract_verbose::<Task>(text).unwrap();
    assert_eq!(result.value.target, "lib.rs");
    assert!(!result.corrections.is_empty());
}
