"""
Tests for koci CLI tool.
"""

import os
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock

import pytest
from koci.main import KociTest
from koci.core import (
    parse_pipeline,
    parse_pipeline_string,
    Pipeline,
    Stage,
    Job,
    ParserError,
    ValidationError,
)
from koci.core.expressions import (
    ExecutionContext,
    evaluate_when,
    evaluate_if,
    evaluate_condition,
    ExpressionError,
)
from koci.core.models import (
    Step, Artifact, Service, MatrixConfig, JobStatus,
    ReportConfig, CoverageConfig,
)
from koci.core.reports import (
    JUnitParser, CoverageParser, JUnitResult, CoverageResult,
    ReportResult, TestCase, parse_reports,
)


# Sample pipeline YAML for testing
SAMPLE_PIPELINE = """
version: "1"
name: test-pipeline

stages:
  - build
  - test

jobs:
  compile:
    stage: build
    image: node:20
    steps:
      - name: Install
        run: npm install
      - name: Build
        run: npm run build
    artifacts:
      paths:
        - dist/

  unit-tests:
    stage: test
    image: node:20
    needs: [compile]
    steps:
      - run: npm test
    artifacts:
      consume: [compile]
"""


class TestKociCLI:
    """Test CLI commands."""

    def test_koci_help(self):
        """Test koci without any subcommands."""
        with KociTest() as app:
            app.run()
            assert app.exit_code == 0

    def test_koci_debug(self):
        """Test that debug mode is functional."""
        argv = ['--debug']
        with KociTest(argv=argv) as app:
            app.run()
            assert app.debug is True

    def test_validate_with_file(self):
        """Test validate command with a pipeline file."""
        with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False) as f:
            f.write(SAMPLE_PIPELINE)
            f.flush()

            try:
                argv = ['validate', '-f', f.name]
                with KociTest(argv=argv) as app:
                    app.run()
                    assert app.exit_code == 0
            finally:
                os.unlink(f.name)

    def test_info_command(self):
        """Test info command."""
        with tempfile.NamedTemporaryFile(mode='w', suffix='.yml', delete=False) as f:
            f.write(SAMPLE_PIPELINE)
            f.flush()

            try:
                argv = ['info', '-f', f.name]
                with KociTest(argv=argv) as app:
                    app.run()
                    assert app.exit_code == 0
            finally:
                os.unlink(f.name)


class TestPipelineParser:
    """Test pipeline parsing."""

    def test_parse_simple_pipeline(self):
        """Test parsing a simple pipeline."""
        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)

        assert pipeline.name == 'test-pipeline'
        assert pipeline.version == '1'
        assert len(pipeline.stages) == 2
        assert pipeline.stages[0].name == 'build'
        assert pipeline.stages[1].name == 'test'

    def test_parse_jobs(self):
        """Test parsing jobs."""
        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)

        jobs = pipeline.get_all_jobs()
        assert len(jobs) == 2

        compile_job = pipeline.get_job('compile')
        assert compile_job is not None
        assert compile_job.image == 'node:20'
        assert len(compile_job.steps) == 2
        assert compile_job.artifacts.paths == ['dist/']

    def test_parse_dependencies(self):
        """Test parsing job dependencies."""
        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)

        test_job = pipeline.get_job('unit-tests')
        assert test_job is not None
        assert test_job.needs == ['compile']
        assert test_job.artifacts.consume == ['compile']

    def test_invalid_yaml(self):
        """Test parsing invalid YAML."""
        with pytest.raises(ParserError):
            parse_pipeline_string("invalid: yaml: content: [")

    def test_missing_stages(self):
        """Test validation with missing stages."""
        invalid_pipeline = """
version: "1"
name: test

jobs:
  build:
    stage: build
    steps:
      - run: echo hello
"""
        with pytest.raises(ValidationError):
            parse_pipeline_string(invalid_pipeline)

    def test_parse_with_reports(self):
        """Test parsing jobs with report configuration."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.12
    steps:
      - run: pytest
    reports:
      junit: test-results.xml
      coverage:
        path: coverage.xml
        format: cobertura
        threshold: 80
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')
        assert job.reports is not None
        assert job.reports.junit == 'test-results.xml'
        assert job.reports.coverage.path == 'coverage.xml'
        assert job.reports.coverage.format == 'cobertura'
        assert job.reports.coverage.threshold == 80

    def test_parse_simple_coverage_path(self):
        """Test parsing jobs with simple coverage path."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.12
    steps:
      - run: pytest
    reports:
      coverage: coverage.xml
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')
        assert job.reports.coverage.path == 'coverage.xml'


class TestExpressions:
    """Test expression evaluation."""

    def test_simple_when_branch(self):
        """Test simple branch condition."""
        ctx = ExecutionContext(branch='main')

        assert evaluate_when("branch == 'main'", ctx) is True
        assert evaluate_when("branch == 'develop'", ctx) is False
        assert evaluate_when("branch != 'develop'", ctx) is True

    def test_when_pattern_match(self):
        """Test pattern matching in when conditions."""
        ctx = ExecutionContext(branch='feature/test')

        assert evaluate_when("branch =~ 'feature/*'", ctx) is True
        assert evaluate_when("branch =~ 'release/*'", ctx) is False

    def test_when_always_never(self):
        """Test always and never keywords."""
        ctx = ExecutionContext()

        assert evaluate_when("always", ctx) is True
        assert evaluate_when("never", ctx) is False

    def test_when_compound(self):
        """Test compound conditions."""
        ctx = ExecutionContext(branch='main', tag='v1.0.0')

        assert evaluate_when("branch == 'main' && tag =~ 'v*'", ctx) is True
        assert evaluate_when("branch == 'develop' || tag =~ 'v*'", ctx) is True
        assert evaluate_when("branch == 'develop' && tag =~ 'v*'", ctx) is False

    def test_if_success_failure(self):
        """Test success() and failure() functions."""
        # All jobs succeeded
        ctx = ExecutionContext(job_results={'build': 'success', 'test': 'success'})
        assert evaluate_if("${{ success() }}", ctx) is True
        assert evaluate_if("${{ failure() }}", ctx) is False

        # One job failed
        ctx = ExecutionContext(job_results={'build': 'success', 'test': 'failed'})
        assert evaluate_if("${{ success() }}", ctx) is False
        assert evaluate_if("${{ failure() }}", ctx) is True

    def test_if_env_comparison(self):
        """Test environment variable comparisons."""
        ctx = ExecutionContext(env={'DEBUG': 'true', 'ENV': 'production'})

        assert evaluate_if("${{ env.DEBUG == 'true' }}", ctx) is True
        assert evaluate_if("${{ env.ENV == 'production' }}", ctx) is True
        assert evaluate_if("${{ env.ENV != 'staging' }}", ctx) is True

    def test_combined_conditions(self):
        """Test combined when and if conditions."""
        ctx = ExecutionContext(
            branch='main',
            env={'DEPLOY': 'true'},
            job_results={'build': 'success'},
        )

        assert evaluate_condition(
            when="branch == 'main'",
            condition="${{ success() }}",
            context=ctx,
        ) is True

        assert evaluate_condition(
            when="branch == 'develop'",
            condition="${{ success() }}",
            context=ctx,
        ) is False

    def test_when_tag_conditions(self):
        """Test tag-based when conditions."""
        ctx = ExecutionContext(tag='v1.2.3')
        assert evaluate_when("tag =~ 'v*'", ctx) is True
        assert evaluate_when("tag == 'v1.2.3'", ctx) is True
        assert evaluate_when("tag != 'v2.0.0'", ctx) is True

    def test_no_condition(self):
        """Test with no conditions."""
        ctx = ExecutionContext()
        assert evaluate_condition(when=None, condition=None, context=ctx) is True


class TestMatrixExpansion:
    """Test matrix job expansion."""

    def test_simple_matrix(self):
        """Test simple matrix expansion."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.11
    matrix:
      python: ["3.9", "3.10", "3.11"]
    steps:
      - run: python --version
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')

        expanded = job.get_expanded_jobs()
        assert len(expanded) == 3

    def test_multi_dimension_matrix(self):
        """Test multi-dimensional matrix expansion."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.11
    matrix:
      python: ["3.9", "3.10"]
      os: ["linux", "macos"]
    steps:
      - run: echo test
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')

        expanded = job.get_expanded_jobs()
        assert len(expanded) == 4  # 2 python × 2 os

    def test_matrix_with_image_substitution(self):
        """Test matrix variable substitution in image."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:${version}
    matrix:
      version: ["3.10", "3.11"]
    steps:
      - run: python --version
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')
        expanded = job.get_expanded_jobs()
        assert len(expanded) == 2
        # Check that image was expanded
        assert 'python:3.10' in [j.image for j in expanded]
        assert 'python:3.11' in [j.image for j in expanded]


class TestExporters:
    """Test CI platform exporters."""

    def test_github_actions_export(self):
        """Test GitHub Actions export."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)

        assert 'name: test-pipeline' in output
        assert 'runs-on: ubuntu-latest' in output
        assert 'actions/checkout@v4' in output
        assert 'actions/upload-artifact@v4' in output

    def test_jenkins_export(self):
        """Test Jenkins export."""
        from koci.exporters.jenkins import JenkinsExporter

        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)
        exporter = JenkinsExporter()
        output = exporter.export(pipeline)

        assert 'pipeline {' in output
        assert "stage('compile')" in output
        assert "docker {" in output

    def test_gitlab_export(self):
        """Test GitLab CI export."""
        from koci.exporters.gitlab import GitLabExporter

        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)
        exporter = GitLabExporter()
        output = exporter.export(pipeline)

        assert 'stages:' in output
        assert '- build' in output
        assert '- test' in output
        assert 'script:' in output

    def test_github_actions_with_reports(self):
        """Test GitHub Actions export with reports."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.12
    steps:
      - run: pytest
    reports:
      junit: results.xml
      coverage:
        path: coverage.xml
        threshold: 80
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert 'dorny/test-reporter' in output
        assert 'codecov/codecov-action' in output

    def test_gitlab_with_reports(self):
        """Test GitLab export with reports."""
        from koci.exporters.gitlab import GitLabExporter

        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.12
    steps:
      - run: pytest
    reports:
      junit: results.xml
      coverage:
        path: coverage.xml
        format: cobertura
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitLabExporter()
        output = exporter.export(pipeline)
        assert 'reports:' in output
        assert 'junit:' in output

    def test_jenkins_with_reports(self):
        """Test Jenkins export with reports."""
        from koci.exporters.jenkins import JenkinsExporter

        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:3.12
    steps:
      - run: pytest
    reports:
      junit: results.xml
      coverage:
        path: coverage.xml
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = JenkinsExporter()
        output = exporter.export(pipeline)
        assert 'junit testResults' in output
        assert 'publishCoverage' in output


class TestModels:
    """Test data models."""

    def test_step_name_generation(self):
        """Test Step auto-generates name from command."""
        step = Step(run="npm install")
        assert step.name == "npm install"

        # Test truncation
        long_cmd = "a" * 100
        step = Step(run=long_cmd)
        assert len(step.name) <= 53  # 50 + "..."

    def test_artifact_properties(self):
        """Test Artifact has_outputs and has_inputs."""
        artifact = Artifact(paths=['dist/'], consume=['build'])
        assert artifact.has_outputs is True
        assert artifact.has_inputs is True

        empty_artifact = Artifact()
        assert empty_artifact.has_outputs is False
        assert empty_artifact.has_inputs is False

    def test_service_name_generation(self):
        """Test Service auto-generates name from image."""
        service = Service(image='postgres:15')
        assert service.name == 'postgres'

        service = Service(image='docker.io/library/redis:7')
        assert service.name == 'redis'

    def test_job_status_enum(self):
        """Test JobStatus enum values."""
        assert JobStatus.PENDING.value == 'pending'
        assert JobStatus.SUCCESS.value == 'success'
        assert JobStatus.FAILED.value == 'failed'
        assert JobStatus.SKIPPED.value == 'skipped'

    def test_report_config(self):
        """Test ReportConfig model."""
        config = ReportConfig(
            junit='results.xml',
            coverage=CoverageConfig(path='cov.xml', threshold=80)
        )
        assert config.has_junit is True
        assert config.has_coverage is True
        assert config.coverage.threshold == 80

    def test_pipeline_methods(self):
        """Test Pipeline helper methods."""
        pipeline = parse_pipeline_string(SAMPLE_PIPELINE)

        # Test get_stage
        build_stage = pipeline.get_stage('build')
        assert build_stage is not None
        assert build_stage.name == 'build'
        assert pipeline.get_stage('nonexistent') is None

        # Test get_stage_names
        names = pipeline.get_stage_names()
        assert names == ['build', 'test']


class TestJUnitParser:
    """Test JUnit XML parsing."""

    def test_parse_junit_xml(self):
        """Test parsing JUnit XML file."""
        junit_xml = '''<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="3" failures="1" errors="0" skipped="1" time="2.5">
    <testsuite name="MyTests" tests="3" failures="1" errors="0" skipped="1" time="2.5">
        <testcase name="test_pass" classname="MyTests" time="0.5"/>
        <testcase name="test_fail" classname="MyTests" time="1.0">
            <failure message="Expected 1 but got 2">AssertionError</failure>
        </testcase>
        <testcase name="test_skip" classname="MyTests" time="0.0">
            <skipped message="Not implemented"/>
        </testcase>
    </testsuite>
</testsuites>'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(junit_xml)
            f.flush()

            try:
                parser = JUnitParser()
                result = parser.parse(f.name)

                assert result.tests == 3
                assert result.passed == 1
                assert result.failures == 1
                assert result.skipped == 1
                assert result.success is False
                assert len(result.get_failed_tests()) == 1
            finally:
                os.unlink(f.name)

    def test_parse_junit_single_testsuite(self):
        """Test parsing JUnit with single testsuite root."""
        junit_xml = '''<?xml version="1.0"?>
<testsuite name="Tests" tests="2" failures="0" errors="0" skipped="0" time="1.0">
    <testcase name="test_one" classname="Tests" time="0.5"/>
    <testcase name="test_two" classname="Tests" time="0.5"/>
</testsuite>'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(junit_xml)
            f.flush()

            try:
                parser = JUnitParser()
                result = parser.parse(f.name)
                assert result.tests == 2
                assert result.passed == 2
                assert result.success is True
            finally:
                os.unlink(f.name)

    def test_parse_nonexistent_file(self):
        """Test parsing non-existent file."""
        parser = JUnitParser()
        result = parser.parse('/nonexistent/path.xml')
        assert result.tests == 0

    def test_parse_glob_pattern(self):
        """Test parsing with glob pattern."""
        with tempfile.TemporaryDirectory() as tmpdir:
            # Create multiple XML files
            for i in range(2):
                xml = f'''<testsuite tests="1" failures="0">
                    <testcase name="test_{i}" classname="T" time="0.1"/>
                </testsuite>'''
                with open(os.path.join(tmpdir, f'test{i}.xml'), 'w') as f:
                    f.write(xml)

            parser = JUnitParser()
            result = parser.parse(os.path.join(tmpdir, '*.xml'))
            assert result.tests == 2

    def test_junit_result_summary(self):
        """Test JUnitResult summary method."""
        result = JUnitResult(
            tests=10, passed=8, failures=1, errors=0, skipped=1, time=5.5
        )
        summary = result.summary()
        assert '8 passed' in summary
        assert '1 failed' in summary
        assert '1 skipped' in summary
        assert '5.5s' in summary

    def test_junit_with_error(self):
        """Test parsing JUnit with error element."""
        junit_xml = '''<testsuite tests="1" failures="0" errors="1">
    <testcase name="test_error" classname="T">
        <error message="RuntimeError">Traceback...</error>
    </testcase>
</testsuite>'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(junit_xml)
            f.flush()

            try:
                parser = JUnitParser()
                result = parser.parse(f.name)
                assert result.errors == 1
                failed = result.get_failed_tests()
                assert len(failed) == 1
                assert failed[0].status == 'error'
            finally:
                os.unlink(f.name)


class TestCoverageParser:
    """Test coverage report parsing."""

    def test_parse_cobertura_xml(self):
        """Test parsing Cobertura XML."""
        cobertura_xml = '''<?xml version="1.0" ?>
<coverage line-rate="0.85" branch-rate="0.7" version="1.0">
    <packages>
        <package name="mypackage">
            <classes>
                <class name="mymodule.py">
                    <lines>
                        <line number="1" hits="1"/>
                        <line number="2" hits="1"/>
                        <line number="3" hits="0"/>
                    </lines>
                </class>
            </classes>
        </package>
    </packages>
</coverage>'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(cobertura_xml)
            f.flush()

            try:
                parser = CoverageParser()
                result = parser.parse(f.name, 'cobertura')

                assert result.line_rate == 0.85
                assert result.line_percent == 85.0
                assert result.format == 'cobertura'
            finally:
                os.unlink(f.name)

    def test_parse_lcov(self):
        """Test parsing lcov format."""
        lcov_content = '''TN:
SF:src/main.py
DA:1,1
DA:2,1
DA:3,0
DA:4,1
LF:4
LH:3
BRF:2
BRH:1
end_of_record'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.info', delete=False) as f:
            f.write(lcov_content)
            f.flush()

            try:
                parser = CoverageParser()
                result = parser.parse(f.name, 'lcov')

                assert result.lines_total == 4
                assert result.lines_covered == 3
                assert result.line_rate == 0.75
                assert result.format == 'lcov'
            finally:
                os.unlink(f.name)

    def test_coverage_auto_detect_cobertura(self):
        """Test auto-detecting Cobertura format."""
        xml = '<?xml version="1.0"?><coverage line-rate="0.5"></coverage>'
        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(xml)
            f.flush()

            try:
                parser = CoverageParser()
                result = parser.parse(f.name, 'auto')
                assert result.format == 'cobertura'
            finally:
                os.unlink(f.name)

    def test_coverage_auto_detect_lcov(self):
        """Test auto-detecting lcov format."""
        lcov = 'TN:\nSF:test.py\nLF:10\nLH:8\nend_of_record'
        with tempfile.NamedTemporaryFile(mode='w', suffix='.info', delete=False) as f:
            f.write(lcov)
            f.flush()

            try:
                parser = CoverageParser()
                result = parser.parse(f.name, 'auto')
                assert result.format == 'lcov'
            finally:
                os.unlink(f.name)

    def test_coverage_meets_threshold(self):
        """Test coverage threshold checking."""
        result = CoverageResult(line_rate=0.85)
        assert result.meets_threshold(80) is True
        assert result.meets_threshold(90) is False

    def test_coverage_summary(self):
        """Test CoverageResult summary."""
        result = CoverageResult(
            line_rate=0.85,
            branch_rate=0.70,
            branches_total=10,
            branches_covered=7
        )
        summary = result.summary()
        assert '85.0% lines' in summary
        assert '70.0% branches' in summary

    def test_parse_nonexistent_coverage(self):
        """Test parsing non-existent coverage file."""
        parser = CoverageParser()
        result = parser.parse('/nonexistent/coverage.xml')
        assert result.lines_total == 0


class TestReportResult:
    """Test ReportResult dataclass."""

    def test_report_result_properties(self):
        """Test ReportResult has_junit and has_coverage."""
        result = ReportResult()
        assert result.has_junit is False
        assert result.has_coverage is False

        result.junit = JUnitResult(tests=10)
        assert result.has_junit is True

        result.coverage = CoverageResult(line_rate=0.8)
        assert result.has_coverage is True


class TestParseReports:
    """Test the parse_reports convenience function."""

    def test_parse_reports_function(self):
        """Test parse_reports with both JUnit and coverage."""
        junit_xml = '<testsuite tests="1"><testcase name="t"/></testsuite>'
        cov_xml = '<?xml version="1.0"?><coverage line-rate="0.9"></coverage>'

        with tempfile.TemporaryDirectory() as tmpdir:
            junit_path = os.path.join(tmpdir, 'junit.xml')
            cov_path = os.path.join(tmpdir, 'cov.xml')

            with open(junit_path, 'w') as f:
                f.write(junit_xml)
            with open(cov_path, 'w') as f:
                f.write(cov_xml)

            result = parse_reports(junit_path=junit_path, coverage_path=cov_path)
            assert result.has_junit is True
            assert result.has_coverage is True


class TestTestCase:
    """Test TestCase dataclass."""

    def test_test_case_defaults(self):
        """Test TestCase default values."""
        tc = TestCase(name='test', classname='TestClass')
        assert tc.status == 'passed'
        assert tc.time == 0.0
        assert tc.message is None
        assert tc.output is None


class TestMatrixConfig:
    """Test MatrixConfig."""

    def test_matrix_expand_empty(self):
        """Test expanding empty matrix."""
        config = MatrixConfig()
        assert config.expand() == [{}]

    def test_matrix_with_exclude(self):
        """Test matrix expansion with excludes."""
        config = MatrixConfig(
            variables={'a': [1, 2], 'b': ['x', 'y']},
            exclude=[{'a': 1, 'b': 'x'}]
        )
        combinations = config.expand()
        assert {'a': 1, 'b': 'x'} not in combinations
        assert len(combinations) == 3

    def test_matrix_with_include(self):
        """Test matrix expansion with includes."""
        config = MatrixConfig(
            variables={'a': [1]},
            include=[{'a': 2, 'b': 'special'}]
        )
        combinations = config.expand()
        assert {'a': 2, 'b': 'special'} in combinations


class TestJobExpansion:
    """Test job matrix expansion with reports."""

    def test_job_expansion_preserves_reports(self):
        """Test that matrix expansion preserves reports config."""
        pipeline_yaml = """
version: "1"
name: test

stages:
  - test

jobs:
  test:
    stage: test
    image: python:${VER}
    matrix:
      VER: ["3.10", "3.11"]
    steps:
      - run: pytest
    reports:
      junit: results.xml
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('test')
        expanded = job.get_expanded_jobs()

        for exp_job in expanded:
            assert exp_job.reports is not None
            assert exp_job.reports.junit == 'results.xml'


class TestExporterConditions:
    """Test exporter condition conversions."""

    def test_github_actions_branch_condition(self):
        """Test GitHub Actions branch condition."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - deploy
jobs:
  deploy:
    stage: deploy
    image: alpine
    when: branch == 'main'
    steps:
      - run: echo deploy
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert "github.ref == 'refs/heads/main'" in output

    def test_github_actions_tag_condition(self):
        """Test GitHub Actions tag condition."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - deploy
jobs:
  deploy:
    stage: deploy
    image: alpine
    when: tag =~ 'v*'
    steps:
      - run: echo deploy
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert "startsWith(github.ref, 'refs/tags/v')" in output

    def test_gitlab_branch_condition(self):
        """Test GitLab branch condition."""
        from koci.exporters.gitlab import GitLabExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - deploy
jobs:
  deploy:
    stage: deploy
    image: alpine
    when: branch == 'main'
    steps:
      - run: echo deploy
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitLabExporter()
        output = exporter.export(pipeline)
        assert "$CI_COMMIT_BRANCH == 'main'" in output

    def test_jenkins_branch_condition(self):
        """Test Jenkins branch condition."""
        from koci.exporters.jenkins import JenkinsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - deploy
jobs:
  deploy:
    stage: deploy
    image: alpine
    when: branch == 'main'
    steps:
      - run: echo deploy
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = JenkinsExporter()
        output = exporter.export(pipeline)
        assert "branch 'main'" in output


class TestExporterServices:
    """Test exporter service handling."""

    def test_github_actions_services(self):
        """Test GitHub Actions service export."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - test
services:
  test:
    - image: postgres:15
      env:
        POSTGRES_PASSWORD: test
      ports:
        - "5432:5432"
jobs:
  test:
    stage: test
    image: node:20
    steps:
      - run: npm test
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert 'services:' in output
        assert 'postgres:15' in output

    def test_gitlab_services(self):
        """Test GitLab service export."""
        from koci.exporters.gitlab import GitLabExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - test
services:
  test:
    - image: redis:7
      name: cache
jobs:
  test:
    stage: test
    image: node:20
    steps:
      - run: npm test
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitLabExporter()
        output = exporter.export(pipeline)
        assert 'services:' in output
        assert 'redis:7' in output


class TestExporterMatrix:
    """Test exporter matrix handling."""

    def test_github_actions_matrix(self):
        """Test GitHub Actions matrix export."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - test
jobs:
  test:
    stage: test
    image: python:3.11
    matrix:
      python: ["3.9", "3.10", "3.11"]
    steps:
      - run: python --version
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert 'strategy:' in output
        assert 'matrix:' in output

    def test_jenkins_matrix(self):
        """Test Jenkins matrix export."""
        from koci.exporters.jenkins import JenkinsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - test
jobs:
  test:
    stage: test
    image: python:3.11
    matrix:
      python: ["3.9", "3.10"]
    steps:
      - run: python --version
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = JenkinsExporter()
        output = exporter.export(pipeline)
        assert 'matrix {' in output
        assert 'axis {' in output


class TestExporterTriggers:
    """Test exporter trigger handling."""

    def test_github_actions_triggers(self):
        """Test GitHub Actions trigger export."""
        from koci.exporters.github_actions import GitHubActionsExporter

        pipeline_yaml = """
version: "1"
name: test
stages:
  - test
jobs:
  test:
    stage: test
    image: alpine
    steps:
      - run: echo test
on:
  push:
    branches: [main, develop]
    tags: ["v*"]
  pull_request:
    branches: [main]
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        exporter = GitHubActionsExporter()
        output = exporter.export(pipeline)
        assert 'on:' in output
        assert 'push:' in output
        assert 'pull_request:' in output


class TestExpressionEdgeCases:
    """Test expression evaluation edge cases."""

    def test_when_empty_context(self):
        """Test when with empty context."""
        ctx = ExecutionContext()
        assert evaluate_when("branch == 'main'", ctx) is False
        assert evaluate_when("tag =~ 'v*'", ctx) is False

    def test_if_empty_job_results(self):
        """Test if with empty job results."""
        ctx = ExecutionContext(job_results={})
        assert evaluate_if("${{ success() }}", ctx) is True

    def test_if_always_function(self):
        """Test always() function in if condition."""
        ctx = ExecutionContext(job_results={'build': 'failed'})
        assert evaluate_if("${{ always() }}", ctx) is True

    def test_when_negation(self):
        """Test negation in when condition."""
        ctx = ExecutionContext(branch='develop')
        assert evaluate_when("branch != 'main'", ctx) is True


class TestParserEdgeCases:
    """Test parser edge cases."""

    def test_parse_with_global_env(self):
        """Test parsing pipeline with global env."""
        pipeline_yaml = """
version: "1"
name: test
env:
  NODE_ENV: production
  CI: "true"
stages:
  - build
jobs:
  build:
    stage: build
    image: node:20
    steps:
      - run: echo $NODE_ENV
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        assert pipeline.env['NODE_ENV'] == 'production'
        assert pipeline.env['CI'] == 'true'

    def test_parse_with_defaults(self):
        """Test parsing pipeline with defaults."""
        pipeline_yaml = """
version: "1"
name: test
defaults:
  image: node:20
  working_directory: /app
stages:
  - build
jobs:
  build:
    stage: build
    steps:
      - run: npm install
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('build')
        assert job.image == 'node:20'
        assert job.working_directory == '/app'

    def test_parse_step_with_options(self):
        """Test parsing step with all options."""
        pipeline_yaml = """
version: "1"
name: test
stages:
  - build
jobs:
  build:
    stage: build
    image: python:3.12
    steps:
      - name: Complex step
        run: pytest
        working_directory: /tests
        shell: bash
        continue_on_error: true
        timeout_minutes: 30
        env:
          DEBUG: "true"
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('build')
        step = job.steps[0]
        assert step.name == 'Complex step'
        assert step.working_directory == '/tests'
        assert step.shell == 'bash'
        assert step.continue_on_error is True
        assert step.timeout_minutes == 30
        assert step.env['DEBUG'] == 'true'

    def test_parse_job_with_all_options(self):
        """Test parsing job with all options."""
        pipeline_yaml = """
version: "1"
name: test
stages:
  - build
jobs:
  build:
    stage: build
    image: python:3.12
    timeout_minutes: 120
    continue_on_error: true
    retry: 3
    working_directory: /workspace/app
    env:
      CI: "true"
    steps:
      - run: echo test
"""
        pipeline = parse_pipeline_string(pipeline_yaml)
        job = pipeline.get_job('build')
        assert job.timeout_minutes == 120
        assert job.continue_on_error is True
        assert job.retry == 3
        assert job.working_directory == '/workspace/app'


class TestValidationErrors:
    """Test validation error cases."""

    def test_missing_jobs(self):
        """Test pipeline missing jobs."""
        pipeline_yaml = """
version: "1"
name: test
stages:
  - build
"""
        with pytest.raises(ValidationError):
            parse_pipeline_string(pipeline_yaml)

    def test_job_missing_stage(self):
        """Test job missing stage field."""
        pipeline_yaml = """
version: "1"
name: test
stages:
  - build
jobs:
  build:
    image: alpine
    steps:
      - run: echo test
"""
        with pytest.raises(ValidationError):
            parse_pipeline_string(pipeline_yaml)

    def test_job_missing_steps(self):
        """Test job missing steps."""
        pipeline_yaml = """
version: "1"
name: test
stages:
  - build
jobs:
  build:
    stage: build
    image: alpine
"""
        with pytest.raises(ValidationError):
            parse_pipeline_string(pipeline_yaml)


class TestContainerConfig:
    """Test container configuration models."""

    def test_container_config_defaults(self):
        """Test ContainerConfig default values."""
        from koci.runtime.container import ContainerConfig

        config = ContainerConfig(image='alpine')
        assert config.image == 'alpine'
        assert config.name is None
        assert config.working_dir == '/workspace'
        assert config.detach is True

    def test_container_info(self):
        """Test ContainerInfo model."""
        from koci.runtime.container import ContainerInfo, RuntimeType

        info = ContainerInfo(
            id='abc123',
            name='test-container',
            image='alpine:latest',
            status='running',
            runtime=RuntimeType.DOCKER
        )
        assert info.id == 'abc123'
        assert info.status == 'running'

    def test_exec_result(self):
        """Test ExecResult model."""
        from koci.runtime.container import ExecResult

        result = ExecResult(exit_code=0, stdout='hello', stderr='')
        assert result.success is True
        assert result.output == 'hello'

        result2 = ExecResult(exit_code=1, stdout='', stderr='error')
        assert result2.success is False
        assert result2.output == 'error'


class TestOutputFormatter:
    """Test output formatter."""

    def test_verbosity_enum(self):
        """Test Verbosity enum."""
        from koci.output.formatter import Verbosity

        assert Verbosity.QUIET.value == 0
        assert Verbosity.NORMAL.value == 1
        assert Verbosity.VERBOSE.value == 2
        assert Verbosity.DEBUG.value == 3


class TestCoverageWithBranches:
    """Test coverage parsing with branch coverage."""

    def test_cobertura_with_branches(self):
        """Test parsing Cobertura with branch coverage."""
        cobertura_xml = '''<?xml version="1.0" ?>
<coverage line-rate="0.9" branch-rate="0.8">
    <packages>
        <package>
            <classes>
                <class>
                    <lines>
                        <line number="1" hits="1" branch="true" condition-coverage="75% (3/4)"/>
                        <line number="2" hits="1"/>
                    </lines>
                </class>
            </classes>
        </package>
    </packages>
</coverage>'''

        with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
            f.write(cobertura_xml)
            f.flush()

            try:
                parser = CoverageParser()
                result = parser.parse(f.name, 'cobertura')
                assert result.branches_covered == 3
                assert result.branches_total == 4
            finally:
                os.unlink(f.name)
