mod error;
mod matcher;
mod model;

use std::path::Path;

pub use error::{DependencyError, ParseError, ValidationError};
pub use model::{
    BranchPattern, Job, Pipeline, PipelineVersion, PullRequestTrigger,
    PushTrigger, RunOn, Runtime, RuntimeType, Step, StepKind, StepParam,
    TriggerEvent,
};

pub fn parse_file(path: &Path) -> Result<Pipeline, ParseError> {
    let content = std::fs::read_to_string(path)?;
    parse_from_str(&content)
}

pub fn parse_from_str(content: &str) -> Result<Pipeline, ParseError> {
    let pipeline: Pipeline = serde_yaml::from_str(content)?;
    Ok(pipeline)
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use super::*;

    fn sample_pipeline_path() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("ep.pipeline.yaml")
    }

    #[test]
    fn parse_sample_pipeline() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();

        assert_eq!(pipeline.version, PipelineVersion::V1);
        assert_eq!(pipeline.name, "container-ci");
        assert_eq!(pipeline.jobs.len(), 3);
    }

    #[test]
    fn parse_test_job() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        let test_job = &pipeline.jobs["test"];

        assert_eq!(test_job.stage, "verify");
        assert!(test_job.depends_on.is_empty());
        assert_eq!(test_job.runtime.runtime_type, RuntimeType::Container);
        assert_eq!(test_job.runtime.image, "node:20-bookworm");
        assert_eq!(test_job.runner_labels, vec!["linux", "container"]);
        assert_eq!(test_job.steps.len(), 2);

        let checkout = &test_job.steps[0];
        assert_eq!(checkout.name, "Checkout");
        assert_eq!(checkout.kind(), Some(StepKind::Task));
        assert_eq!(checkout.task, Some("source.checkout".to_string()));

        let test_step = &test_job.steps[1];
        assert_eq!(test_step.name, "Test");
        assert_eq!(test_step.kind(), Some(StepKind::Command));
        assert_eq!(test_step.command, Some("pnpm test".to_string()));
    }

    #[test]
    fn parse_build_job_with_params() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        let build_job = &pipeline.jobs["build"];

        assert_eq!(build_job.stage, "package");
        assert_eq!(build_job.depends_on, vec!["test"]);

        let upload_step = &build_job.steps[2];
        assert_eq!(upload_step.name, "Upload dist");
        assert_eq!(upload_step.kind(), Some(StepKind::Task));
        assert_eq!(upload_step.task, Some("artifact.upload".to_string()));
        assert_eq!(upload_step.params.get("name").unwrap().as_string(), "dist");
        assert_eq!(
            upload_step.params.get("paths").unwrap().as_list(),
            &["dist/**".to_string()]
        );
    }

    #[test]
    fn parse_image_job() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        let image_job = &pipeline.jobs["image"];

        assert_eq!(image_job.stage, "package");
        assert_eq!(image_job.depends_on, vec!["build"]);
        assert_eq!(image_job.runtime.runtime_type, RuntimeType::Container);
        assert_eq!(image_job.runtime.image, "docker:27-cli");
        assert_eq!(image_job.runner_labels, vec!["linux", "docker"]);

        let build_image_step = &image_job.steps[2];
        assert_eq!(build_image_step.name, "Build image");
        assert_eq!(build_image_step.kind(), Some(StepKind::Task));
        assert_eq!(
            build_image_step
                .params
                .get("dockerfile")
                .unwrap()
                .as_string(),
            "Dockerfile"
        );
        assert_eq!(
            build_image_step.params.get("tags").unwrap().as_list(),
            &["registry.example.com/app:${commit.sha}".to_string()]
        );
    }

    #[test]
    fn parse_run_on() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        let run_on = pipeline.run_on.as_ref().unwrap();

        let push = run_on.push.as_ref().unwrap();
        assert_eq!(push.branches.len(), 2);
        assert_eq!(push.branches[0].as_str(), "main");
        assert_eq!(push.branches[1].as_str(), "release/*");
        assert_eq!(push.tags.len(), 1);
        assert_eq!(push.tags[0].as_str(), "v*");

        let pr = run_on.pull_request.as_ref().unwrap();
        assert_eq!(pr.branches.len(), 2);
        assert_eq!(pr.branches[0].as_str(), "main");
        assert_eq!(pr.branches[1].as_str(), "feature/*");
    }

    #[test]
    fn stages_and_jobs_by_stage() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();

        let stages = pipeline.stages();
        assert_eq!(stages, vec!["verify", "package"]);

        let verify_jobs = pipeline.jobs_by_stage("verify");
        assert_eq!(verify_jobs.len(), 1);

        let package_jobs = pipeline.jobs_by_stage("package");
        assert_eq!(package_jobs.len(), 2);
    }

    #[test]
    fn execution_order() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        let order = pipeline.execution_order().unwrap();

        let test_idx = order.iter().position(|j| j == "test").unwrap();
        let build_idx = order.iter().position(|j| j == "build").unwrap();
        let image_idx = order.iter().position(|j| j == "image").unwrap();
        assert!(test_idx < build_idx);
        assert!(build_idx < image_idx);
    }

    #[test]
    fn validate_sample_pipeline() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(pipeline.validate().is_ok());
    }

    #[test]
    fn validation_empty_step() {
        let yaml = r#"
version: "1"
name: "bad-pipeline"
jobs:
  build:
    stage: package
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "NoAction"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        let err = pipeline.validate().unwrap_err();
        assert!(matches!(err, ValidationError::EmptyStep { .. }));
    }

    #[test]
    fn validation_unknown_dependency() {
        let yaml = r#"
version: "1"
name: "bad-pipeline"
jobs:
  build:
    stage: package
    depends_on:
      - nonexistent
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "Build"
        command: "echo hi"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        let err = pipeline.execution_order().unwrap_err();
        assert!(matches!(err, DependencyError::UnknownReference { .. }));
    }

    #[test]
    fn validation_circular_dependency() {
        let yaml = r#"
version: "1"
name: "bad-pipeline"
jobs:
  a:
    stage: build
    depends_on:
      - b
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "A"
        command: "echo a"
  b:
    stage: build
    depends_on:
      - a
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "B"
        command: "echo b"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        let err = pipeline.execution_order().unwrap_err();
        assert!(matches!(err, DependencyError::CircularDependency { .. }));
    }

    #[test]
    fn parse_from_str_unknown_version() {
        let yaml = r#"
version: "99"
name: "test"
jobs: {}
"#;
        assert!(parse_from_str(yaml).is_err());
    }

    #[test]
    fn parse_from_str_unknown_runtime_type() {
        let yaml = r#"
version: "1"
name: "test"
jobs:
  build:
    stage: package
    runtime:
      type: unknown
      image: "node:20"
    steps:
      - name: "Build"
        command: "echo hi"
"#;
        assert!(parse_from_str(yaml).is_err());
    }

    #[test]
    fn should_run_push_main() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(pipeline.should_run(&TriggerEvent::PushBranch("main".into())));
    }

    #[test]
    fn should_run_push_release_branch() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(
            pipeline
                .should_run(&TriggerEvent::PushBranch("release/1.0".into()))
        );
    }

    #[test]
    fn should_run_push_feature_branch_rejected() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(
            !pipeline
                .should_run(&TriggerEvent::PushBranch("feature/login".into()))
        );
    }

    #[test]
    fn should_run_push_tag() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(pipeline.should_run(&TriggerEvent::PushTag("v1.0.0".into())));
        assert!(!pipeline.should_run(&TriggerEvent::PushTag("1.0.0".into())));
    }

    #[test]
    fn should_run_pull_request_main() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(pipeline.should_run(&TriggerEvent::PullRequest {
            target: "main".into()
        }));
    }

    #[test]
    fn should_run_pull_request_feature() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(pipeline.should_run(&TriggerEvent::PullRequest {
            target: "feature/login".into()
        }));
    }

    #[test]
    fn should_run_pull_request_develop_rejected() {
        let pipeline = parse_file(&sample_pipeline_path()).unwrap();
        assert!(!pipeline.should_run(&TriggerEvent::PullRequest {
            target: "develop".into()
        }));
    }

    #[test]
    fn should_run_no_run_on_always_true() {
        let yaml = r#"
version: "1"
name: "always-run"
jobs:
  build:
    stage: package
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "Build"
        command: "echo hi"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        assert!(
            pipeline.should_run(&TriggerEvent::PushBranch("anything".into()))
        );
        assert!(pipeline.should_run(&TriggerEvent::PullRequest {
            target: "anything".into()
        }));
    }

    #[test]
    fn should_run_push_with_empty_branches_matches_all() {
        let yaml = r#"
version: "1"
name: "push-any-branch"
run_on:
  push: {}
jobs:
  build:
    stage: package
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "Build"
        command: "echo hi"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        assert!(
            pipeline.should_run(&TriggerEvent::PushBranch("anything".into()))
        );
        assert!(pipeline.should_run(&TriggerEvent::PushTag("v1".into())));
    }

    #[test]
    fn should_run_push_only_no_pull_request() {
        let yaml = r#"
version: "1"
name: "push-only"
run_on:
  push:
    branches:
      - main
jobs:
  build:
    stage: package
    runtime:
      type: container
      image: "node:20"
    steps:
      - name: "Build"
        command: "echo hi"
"#;
        let pipeline = parse_from_str(yaml).unwrap();
        assert!(pipeline.should_run(&TriggerEvent::PushBranch("main".into())));
        assert!(!pipeline.should_run(&TriggerEvent::PullRequest {
            target: "main".into()
        }));
    }

    #[test]
    fn branch_pattern_exact_and_glob() {
        let exact = BranchPattern::new("main");
        assert!(!exact.is_pattern());
        assert!(exact.matches("main"));
        assert!(!exact.matches("develop"));

        let glob = BranchPattern::new("release/*");
        assert!(glob.is_pattern());
        assert!(glob.matches("release/1.0"));
        assert!(!glob.matches("release/1.0/fix"));
        assert!(!glob.matches("main"));

        let deep = BranchPattern::new("feature/**");
        assert!(deep.is_pattern());
        assert!(deep.matches("feature/login"));
        assert!(deep.matches("feature/login/subtask"));
    }
}
