use ito_core::change_repository::FsChangeRepository;
use ito_core::module_repository::FsModuleRepository;
use ito_core::validate::{
    validate_change, validate_module, validate_spec_markdown, validate_tasks_file,
};
use std::path::Path;

fn write(path: &Path, contents: &str) {
    let Some(parent) = path.parent() else {
        panic!("path has no parent: {}", path.display());
    };
    std::fs::create_dir_all(parent).unwrap();
    std::fs::write(path, contents).unwrap();
}

#[test]
fn validate_spec_markdown_reports_missing_purpose_and_requirements() {
    let md = r#"
## Purpose

## Requirements
"#;

    let r = validate_spec_markdown(md, false);
    assert!(!r.valid);
    assert!(r.summary.errors >= 1);
}

#[test]
fn validate_spec_markdown_strict_treats_warnings_as_invalid() {
    let md = r#"
## Purpose

Too short.

## Requirements

### Requirement: R
The system SHALL do it.

#### Scenario: S
ok
"#;

    let non_strict = validate_spec_markdown(md, false);
    assert!(non_strict.valid);
    assert!(non_strict.summary.warnings >= 1);

    let strict = validate_spec_markdown(md, true);
    assert!(!strict.valid);
    assert!(strict.summary.warnings >= 1);
}

#[test]
fn validate_change_requires_at_least_one_delta() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    std::fs::create_dir_all(ito.join("changes").join("001-01_demo")).unwrap();
    let change_repo = FsChangeRepository::new(&ito);

    let r = validate_change(&change_repo, &ito, "001-01_demo", false).unwrap();
    assert!(!r.valid);
    assert!(
        r.issues
            .iter()
            .any(|i| i.message.contains("at least one delta"))
    );
}

#[test]
fn validate_change_requires_shall_or_must_in_requirement_text() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let change_repo = FsChangeRepository::new(&ito);

    write(
        &ito.join("changes")
            .join(change_id)
            .join("specs")
            .join("auth")
            .join("spec.md"),
        r#"
## ADDED Requirements

### Requirement: R
This requirement has no keywords.

#### Scenario: S
ok
"#,
    );

    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(!r.valid);
    assert!(
        r.issues
            .iter()
            .any(|i| i.message.contains("SHALL") || i.message.contains("MUST"))
    );
}

#[test]
fn validate_change_with_unknown_schema_and_no_validation_yaml_does_not_require_deltas() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    // Project-local schema without validation.yaml.
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("unknown")
            .join("schema.yaml"),
        r#"
name: unknown
version: 1
artifacts:
  - id: notes
    generates: notes.md
    template: notes.md
    requires: []
apply:
  requires: [notes]
"#,
    );

    // Change selects the unknown schema.
    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: unknown\n",
    );

    // Create the apply-required artifact so we only test delta validation behavior.
    write(
        &ito.join("changes").join(change_id).join("notes.md"),
        "ok\n",
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        !r.issues
            .iter()
            .any(|i| i.message.contains("at least one delta")),
        "unknown schema should not require deltas, got issues: {:?}",
        r.issues
    );
    assert!(
        r.issues
            .iter()
            .any(|i| i.message.contains("manual validation required")),
        "unknown schema should emit manual validation issue, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_change_uses_validation_yaml_delta_specs_validator_when_configured() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    // Project-local schema with validation.yaml.
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("delta")
            .join("schema.yaml"),
        r#"
name: delta
version: 1
artifacts:
  - id: specs
    generates: specs/**/*.md
    template: specs/spec.md
    requires: []
"#,
    );
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("delta")
            .join("validation.yaml"),
        r#"
version: 1
artifacts:
  specs:
    required: true
    validate_as: ito.delta-specs.v1
"#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: delta\n",
    );
    write(
        &ito.join("changes")
            .join(change_id)
            .join("specs")
            .join("auth")
            .join("spec.md"),
        r#"
## ADDED Requirements

### Requirement: R
No keywords here.

#### Scenario: S
ok
"#,
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        r.issues
            .iter()
            .any(|i| i.message.contains("SHALL") || i.message.contains("MUST")),
        "delta validator should run, got issues: {:?}",
        r.issues
    );
    assert!(
        !r.issues
            .iter()
            .any(|i| i.message.contains("manual validation required")),
        "schema with validation.yaml should not emit manual-validation issue, got issues: {:?}",
        r.issues
    );

    assert!(
        r.issues
            .iter()
            .any(|i| i.message.contains("ito.delta-specs.v1")),
        "expected delta spec issues to cite validator id, got issues: {:?}",
        r.issues
    );

    let with_meta = r.issues.iter().any(|i| {
        let Some(meta) = i.metadata.as_ref().and_then(|m| m.as_object()) else {
            return false;
        };
        meta.get("validator_id").and_then(|v| v.as_str()) == Some("ito.delta-specs.v1")
            && meta.get("spec_path").and_then(|v| v.as_str())
                == Some(".ito/specs/delta-specs/spec.md")
    });
    assert!(
        with_meta,
        "expected delta spec issues to include metadata references"
    );
}

#[test]
fn validate_change_with_validation_yaml_and_no_delta_validator_does_not_require_deltas() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("nodeltas")
            .join("schema.yaml"),
        r#"
name: nodeltas
version: 1
artifacts:
  - id: notes
    generates: notes.md
    template: notes.md
    requires: []
"#,
    );
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("nodeltas")
            .join("validation.yaml"),
        "version: 1\n",
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: nodeltas\n",
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        !r.issues
            .iter()
            .any(|i| i.message.contains("at least one delta")),
        "schema validation without delta validator should not require deltas, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_change_validates_apply_tracks_file_when_configured() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("tracks")
            .join("schema.yaml"),
        r#"
name: tracks
version: 1
artifacts:
  - id: proposal
    generates: proposal.md
    template: proposal.md
    requires: []
apply:
  tracks: todo.md
"#,
    );
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("tracks")
            .join("validation.yaml"),
        r#"
version: 1
tracking:
  source: apply_tracks
  required: true
  validate_as: ito.tasks-tracking.v1
"#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: tracks\n",
    );
    write(
        &ito.join("changes").join(change_id).join("todo.md"),
        r#"
# Tasks for: 001-01_demo

## Wave 1

### Task 1.1: Demo

- **Dependencies**: Task 2.1
- **Status**: [ ] pending
"#,
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        !r.issues.iter().any(|i| i.message.contains("tasks.md")),
        "apply.tracks validation should not require tasks.md, got issues: {:?}",
        r.issues
    );
    assert!(
        r.issues
            .iter()
            .any(|i| i.path == "changes/001-01_demo/todo.md" && i.level == "ERROR"),
        "apply.tracks validation should run and report todo.md diagnostics, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_change_uses_apply_tracks_for_legacy_delta_schemas() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("spec-driven")
            .join("schema.yaml"),
        r#"
name: spec-driven
version: 1
artifacts: []
apply:
  tracks: todo.md
        "#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: spec-driven\n",
    );

    write(
        &ito.join("changes")
            .join(change_id)
            .join("specs")
            .join("a")
            .join("spec.md"),
        r#"
## ADDED Requirements

### Requirement: R
The system SHALL do it.

#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
    );

    write(
        &ito.join("changes").join(change_id).join("todo.md"),
        r#"
## Wave 1

### Task 1.1: First

- **Status**: [ ] pending
- **Updated At**: 2026-02-25

### Task 1.1: Duplicate

- **Status**: [ ] pending
- **Updated At**: 2026-02-25
"#,
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        !r.issues.iter().any(|i| i.path.contains("tasks.md")),
        "apply.tracks should override legacy tasks.md validation, got issues: {:?}",
        r.issues
    );
    assert!(
        r.issues
            .iter()
            .any(|i| i.path == "changes/001-01_demo/todo.md"),
        "expected validation to report issues against todo.md, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_change_rejects_unsafe_apply_tracks_for_schema_validation_tracking() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("tracks")
            .join("schema.yaml"),
        r#"
name: tracks
version: 1
artifacts: []
apply:
  tracks: ../todo.md
"#,
    );
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("tracks")
            .join("validation.yaml"),
        r#"
version: 1
tracking:
  source: apply_tracks
  required: true
  validate_as: ito.tasks-tracking.v1
"#,
    );
    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: tracks\n",
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        r.issues
            .iter()
            .any(|i| i.path == "tracking" && i.message.contains("Invalid tracking file path")),
        "expected unsafe apply.tracks to be rejected, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_change_rejects_unsafe_apply_tracks_for_legacy_delta_schemas() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("spec-driven")
            .join("schema.yaml"),
        r#"
name: spec-driven
version: 1
artifacts: []
apply:
  tracks: dir/todo.md
        "#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: spec-driven\n",
    );
    write(
        &ito.join("changes")
            .join(change_id)
            .join("specs")
            .join("a")
            .join("spec.md"),
        r#"
## ADDED Requirements

### Requirement: R
The system SHALL do it.

#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        r.issues
            .iter()
            .any(|i| i.path == "tracking" && i.message.contains("Invalid tracking file path")),
        "expected unsafe apply.tracks to be rejected, got issues: {:?}",
        r.issues
    );
}

#[test]
fn empty_tracking_file_is_warning_in_non_strict_and_error_in_strict() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("spec-driven")
            .join("schema.yaml"),
        r#"
name: spec-driven
version: 1
artifacts: []
        "#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: spec-driven\n",
    );

    write(
        &ito.join("changes")
            .join(change_id)
            .join("specs")
            .join("a")
            .join("spec.md"),
        r#"
## ADDED Requirements

### Requirement: R
The system SHALL do it.

#### Scenario: S
- **WHEN** ok
- **THEN** ok
"#,
    );
    write(
        &ito.join("changes").join(change_id).join("tasks.md"),
        "# just notes\n",
    );

    let change_repo = FsChangeRepository::new(&ito);

    let non_strict = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        non_strict
            .issues
            .iter()
            .any(|i| i.path == "changes/001-01_demo/tasks.md" && i.level == "WARNING"),
        "expected warning for empty tracking file, got issues: {:?}",
        non_strict.issues
    );

    let strict = validate_change(&change_repo, &ito, change_id, true).unwrap();
    assert!(
        strict
            .issues
            .iter()
            .any(|i| i.path == "changes/001-01_demo/tasks.md" && i.level == "ERROR"),
        "expected error for empty tracking file in strict mode, got issues: {:?}",
        strict.issues
    );
}

#[test]
fn validate_change_skips_optional_validator_when_artifact_is_missing() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("optional")
            .join("schema.yaml"),
        r#"
name: optional
version: 1
artifacts:
  - id: tasks
    generates: tasks.md
    template: tasks.md
    requires: []
"#,
    );
    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("optional")
            .join("validation.yaml"),
        r#"
version: 1
artifacts:
  tasks:
    required: false
    validate_as: ito.tasks-tracking.v1
"#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: optional\n",
    );

    let change_repo = FsChangeRepository::new(&ito);
    let r = validate_change(&change_repo, &ito, change_id, false).unwrap();
    assert!(
        !r.issues
            .iter()
            .any(|i| i.message.contains("Failed to read") && i.path.contains("tasks.md")),
        "optional missing artifact should skip validators, got issues: {:?}",
        r.issues
    );
}

#[test]
fn validate_module_reports_missing_scope_and_short_purpose() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    let module_repo = FsModuleRepository::new(&ito);

    write(
        &ito.join("modules").join("006_demo").join("module.md"),
        r#"
## Purpose

Too short.

## Scope
"#,
    );

    let (_name, r) = validate_module(&module_repo, &ito, "006_demo", false).unwrap();
    assert!(!r.valid);
    assert!(r.summary.errors >= 1);
}

#[test]
fn validate_tasks_file_returns_empty_for_valid_tasks() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    let change_id = "001-01_demo";
    let valid_tasks = "## 1. Implementation\n- [ ] 1.1 Do the thing\n- [x] 1.2 Done thing\n";
    write(
        &ito.join("changes").join(change_id).join("tasks.md"),
        valid_tasks,
    );

    let issues =
        validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
    assert!(
        issues.is_empty(),
        "valid tasks file should produce no issues, got: {issues:?}"
    );
}

#[test]
fn validate_tasks_file_returns_error_for_missing_file() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");

    let issues = validate_tasks_file(&ito, "nonexistent-change", false)
        .expect("validate_tasks_file should return Ok with error issues");
    assert!(
        !issues.is_empty(),
        "missing tasks file should produce at least one issue"
    );
    assert!(
        issues[0].level == "ERROR",
        "issue should be an ERROR, got: {}",
        issues[0].level
    );
    assert!(
        issues[0].message.contains("tasks.md"),
        "error message should mention tasks.md, got: {}",
        issues[0].message
    );
}

#[test]
fn validate_tasks_file_returns_diagnostics_for_malformed_content() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    let change_id = "001-01_broken";
    // A tasks file with duplicate task IDs should produce diagnostics
    let malformed_tasks = "\
## Wave 1

### Task 1.1: First task
- **Status**: [x] complete

### Task 1.1: Duplicate ID task
- **Status**: [ ] pending
";
    write(
        &ito.join("changes").join(change_id).join("tasks.md"),
        malformed_tasks,
    );

    let issues =
        validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
    assert!(
        !issues.is_empty(),
        "malformed tasks file should produce diagnostics, got empty"
    );
}

#[test]
fn validate_tasks_file_issues_cite_tasks_tracking_validator_id() {
    let td = tempfile::tempdir().unwrap();
    let ito = td.path().join(".ito");
    let change_id = "001-01_broken";
    let malformed_tasks = "\
## Wave 1\n\n\
### Task 1.1: First task\n\
- **Status**: [x] complete\n\
- **Updated At**: 2026-02-25\n\n\
### Task 1.1: Duplicate ID task\n\
- **Status**: [ ] pending\n\
- **Updated At**: 2026-02-25\n";
    write(
        &ito.join("changes").join(change_id).join("tasks.md"),
        malformed_tasks,
    );

    let issues =
        validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
    assert!(
        !issues.is_empty(),
        "expected tasks diagnostics to be reported"
    );

    let some = issues
        .iter()
        .any(|i| i.message.contains("ito.tasks-tracking.v1"));
    assert!(some, "expected tasks issues to cite validator id");

    let with_meta = issues.iter().any(|i| {
        let Some(meta) = i.metadata.as_ref().and_then(|m| m.as_object()) else {
            return false;
        };
        meta.get("validator_id").and_then(|v| v.as_str()) == Some("ito.tasks-tracking.v1")
            && meta.get("spec_path").and_then(|v| v.as_str())
                == Some(".ito/specs/tasks-tracking/spec.md")
    });
    assert!(
        with_meta,
        "expected tasks issues to include metadata references"
    );
}

#[test]
fn validate_tasks_file_uses_apply_tracks_when_set() {
    let td = tempfile::tempdir().unwrap();
    let project_root = td.path();
    let ito = project_root.join(".ito");
    let change_id = "001-01_demo";

    write(
        &project_root
            .join(".ito")
            .join("templates")
            .join("schemas")
            .join("tracks")
            .join("schema.yaml"),
        r#"
name: tracks
version: 1
artifacts: []
apply:
  tracks: todo.md
"#,
    );

    write(
        &ito.join("changes").join(change_id).join(".ito.yaml"),
        "schema: tracks\n",
    );

    // Duplicate ids should produce tasks-tracking diagnostics.
    write(
        &ito.join("changes").join(change_id).join("todo.md"),
        "## Wave 1\n\n### Task 1.1: First\n- **Status**: [x] complete\n- **Updated At**: 2026-02-25\n\n### Task 1.1: Duplicate\n- **Status**: [ ] pending\n- **Updated At**: 2026-02-25\n",
    );

    let issues =
        validate_tasks_file(&ito, change_id, false).expect("validate_tasks_file should succeed");
    assert!(
        issues
            .iter()
            .any(|i| i.path == "changes/001-01_demo/todo.md"),
        "expected issues to reference todo.md, got: {issues:?}"
    );
    assert!(
        !issues
            .iter()
            .any(|i| i.message.contains("tasks.md") || i.path.contains("tasks.md")),
        "apply.tracks should override tasks.md validation, got: {issues:?}"
    );
}
