import asyncio
import base64
import json
import pytest
from pathlib import Path
from typing import Any, Dict, List, Optional

from evalscope.api.agent.types import ExecResult
from evalscope.api.benchmark import BenchmarkMeta
from evalscope.api.dataset import Sample
from evalscope.api.evaluator import TaskState
from evalscope.api.registry import get_benchmark
from evalscope.benchmarks.gdpval.gdpval_adapter import (
    GDPvalAdapter,
    _GDPvalArtifactEnvironment,
    _relative_deliverable_path,
)
from evalscope.config import TaskConfig
from evalscope.constants import HubType, JudgeStrategy


def make_adapter(local_path: str = '', **extra_params: Any) -> GDPvalAdapter:
    base_extra_params = {
        'max_steps': 250,
        'command_timeout': 180.0,
        'docker_image': 'evalscope/gdpval:latest',
        'auto_build_docker_image': True,
        'network_enabled': True,
        'download_reference_files': False,
    }
    base_extra_params.update(extra_params)
    meta = BenchmarkMeta(
        name='gdpval',
        dataset_id='openai-mirror/gdpval',
        subset_list=['default'],
        default_subset='default',
        eval_split='train',
        prompt_template='{question}',
        metric_list=['submission_ready'],
        extra_params=base_extra_params,
    )
    cfg = TaskConfig(
        datasets=['gdpval'],
        dataset_args={'gdpval': {
            'extra_params': extra_params
        }},
    )
    if local_path:
        cfg.dataset_args['gdpval']['local_path'] = local_path
        meta._update({'local_path': local_path})
    return GDPvalAdapter(benchmark_meta=meta, task_config=cfg)


def test_gdpval_registered_under_short_name() -> None:
    cfg = TaskConfig(
        datasets=['gdpval'], dataset_args={'gdpval': {
            'extra_params': {
                'download_reference_files': False
            }
        }}
    )

    adapter = get_benchmark('gdpval', cfg)

    assert isinstance(adapter, GDPvalAdapter)
    assert adapter.name == 'gdpval'


def test_record_to_sample_uses_modelscope_metadata_and_prompt() -> None:
    adapter = make_adapter()
    sample = adapter.record_to_sample({
        'task_id': 'task-1',
        'sector': 'Finance',
        'occupation': 'Analyst',
        'prompt': 'Create the workbook.',
        'reference_files': ['reference_files/abc123/input.xlsx'],
        'reference_file_urls': ['https://example.test/input.xlsx'],
        'reference_file_hf_uris': ['hf://datasets/openai/gdpval/reference_files/abc123/input.xlsx'],
        'rubric_pretty': 'Rubric',
        'rubric_json': {
            'criteria': []
        },
    })

    assert 'Create the workbook.' in sample.input
    assert '/reference_files/abc123/input.xlsx' in sample.input
    assert 'deliverable_files' in sample.input
    assert sample.metadata['dataset_id'] == 'openai-mirror/gdpval'
    assert sample.metadata['dataset_hub'] == HubType.MODELSCOPE
    assert sample.metadata['reference_paths'] == ['reference_files/input.xlsx']
    assert sample.metadata['sandbox_reference_paths'] == ['/reference_files/abc123/input.xlsx']
    assert [tool.name for tool in sample.tools] == ['bash', 'python_exec']


def test_load_dataset_uses_native_loader_and_caches_submission_records(tmp_path: Path) -> None:
    dataset_path = tmp_path / 'gdpval.csv'
    dataset_path.write_text(
        '\n'.join([
            'task_id,sector,occupation,prompt,reference_files,reference_file_urls,reference_file_hf_uris,rubric_pretty,rubric_json',
            'task-1,Finance,Analyst,Create the workbook.,reference_files/abc123/input.xlsx,https://example.test/input.xlsx,hf://datasets/openai/gdpval/reference_files/abc123/input.xlsx,Rubric,[]',
        ]),
        encoding='utf-8',
    )
    adapter = make_adapter(local_path=str(tmp_path))

    dataset = adapter.load_dataset()['default']

    assert len(dataset) == 1
    assert dataset[0].metadata['task_id'] == 'task-1'
    assert [tool.name for tool in dataset[0].tools] == ['bash', 'python_exec']
    assert adapter._submission_records[0]['task_id'] == 'task-1'


def test_build_reference_volumes_uses_downloaded_file_parents(tmp_path: Path) -> None:
    adapter = make_adapter()
    host_dir = tmp_path / 'reference_files' / 'abc123'
    host_dir.mkdir(parents=True)
    host_file = host_dir / 'input.xlsx'
    host_file.write_bytes(b'data')
    sample = Sample(input='prompt', metadata={'host_reference_files': [str(host_file)]})

    volumes = adapter._build_reference_volumes(sample)

    assert volumes[str(host_dir)] == {'bind': '/reference_files/abc123', 'mode': 'ro'}


def test_resolve_reference_files_skips_empty_download(monkeypatch: Any) -> None:
    adapter = make_adapter()
    sample = Sample(input='prompt', metadata={'reference_files': ['missing.xlsx']})

    class FakeDataset:

        @staticmethod
        def download_file(file_path: str) -> Optional[str]:
            return None

    monkeypatch.setattr(GDPvalAdapter, 'source_dataset', property(lambda self: FakeDataset()))

    adapter._resolve_sample_reference_files([sample])

    assert sample.metadata['host_reference_files'] == []


def test_relative_deliverable_path_rejects_unsafe_paths() -> None:
    assert _relative_deliverable_path('deliverable_files/report.pdf') == 'report.pdf'
    assert _relative_deliverable_path('deliverable_files/nested/report.pdf') == 'nested/report.pdf'
    assert _relative_deliverable_path('/tmp/report.pdf') == ''
    assert _relative_deliverable_path('deliverable_files/../report.pdf') == ''


def test_artifact_environment_extracts_deliverables(tmp_path: Path) -> None:
    metadata: Dict[str, Any] = {}
    fake_env = FakeEnvironment({
        'deliverable_files/report.txt': b'hello',
        'deliverable_files/nested/table.csv': b'a,b\n1,2\n',
    })
    env = _GDPvalArtifactEnvironment(env=fake_env, artifact_dir=tmp_path, metadata=metadata)

    asyncio.run(env.close())

    assert (tmp_path / 'deliverable_files/report.txt').read_bytes() == b'hello'
    assert (tmp_path / 'deliverable_files/nested/table.csv').read_bytes() == b'a,b\n1,2\n'
    assert metadata['deliverable_files'] == [
        {
            'path': 'deliverable_files/report.txt',
            'local_path': str(tmp_path / 'deliverable_files/report.txt'),
        },
        {
            'path': 'deliverable_files/nested/table.csv',
            'local_path': str(tmp_path / 'deliverable_files/nested/table.csv'),
        },
    ]


def test_artifact_environment_handles_listing_failure(tmp_path: Path) -> None:
    metadata: Dict[str, Any] = {}

    class ListingFailureEnvironment(FakeEnvironment):

        async def exec(
            self,
            cmd: List[str],
            *,
            cwd: Optional[str] = None,
            input: Optional[str] = None,
            timeout: Optional[float] = None,
            env: Optional[Dict[str, str]] = None,
        ) -> ExecResult:
            if cmd[:3] == ['test', '-d', 'deliverable_files']:
                return ExecResult(returncode=0, stdout='', stderr='')
            if cmd[:4] == ['find', 'deliverable_files', '-type', 'f']:
                return ExecResult(returncode=1, stdout='', stderr='find failed')
            return await super().exec(cmd, cwd=cwd, input=input, timeout=timeout, env=env)

    env = _GDPvalArtifactEnvironment(env=ListingFailureEnvironment({}), artifact_dir=tmp_path, metadata=metadata)

    asyncio.run(env.close())

    assert metadata['deliverable_files'] == []
    assert metadata['artifact_dir'] == str(tmp_path)


def test_artifact_environment_skips_failed_base64_extract(tmp_path: Path) -> None:
    metadata: Dict[str, Any] = {}

    class Base64FailureEnvironment(FakeEnvironment):

        async def exec(
            self,
            cmd: List[str],
            *,
            cwd: Optional[str] = None,
            input: Optional[str] = None,
            timeout: Optional[float] = None,
            env: Optional[Dict[str, str]] = None,
        ) -> ExecResult:
            if cmd[:3] == ['base64', '-w', '0']:
                return ExecResult(returncode=1, stdout='', stderr='base64 failed')
            return await super().exec(cmd, cwd=cwd, input=input, timeout=timeout, env=env)

    env = _GDPvalArtifactEnvironment(
        env=Base64FailureEnvironment({'deliverable_files/report.txt': b'hello'}),
        artifact_dir=tmp_path,
        metadata=metadata,
    )

    asyncio.run(env.close())

    assert metadata['deliverable_files'] == []
    assert metadata['artifact_dir'] == str(tmp_path)


def test_match_score_marks_submission_ready_with_deliverable() -> None:
    adapter = make_adapter()
    sample = Sample(
        input='prompt',
        target='',
        metadata={'deliverable_files': [{
            'path': 'deliverable_files/report.txt'
        }]},
    )
    state = TaskState(model='mock', sample=sample, completed=True)

    score = adapter.match_score('', '', '', state)

    assert score.value['submission_ready'] == 1.0
    assert score.metadata['deliverable_count'] == 1


def test_calculate_metrics_does_not_run_local_llm_judge_for_gdpval() -> None:
    adapter = make_adapter()
    adapter._task_config.judge_strategy = JudgeStrategy.LLM
    sample = Sample(
        input='Task prompt',
        target='',
        metadata={
            'deliverable_files': [{
                'path': 'deliverable_files/report.txt'
            }],
        },
    )
    state = TaskState(model='mock', sample=sample, completed=True)
    state.output.completion = 'Done.'

    sample_score = adapter.calculate_metrics(state)

    assert sample_score.score.value['submission_ready'] == 1.0
    assert 'acc' not in sample_score.score.value
    assert sample_score.score.main_score_name == 'submission_ready'
    assert 'OpenAI' in sample_score.score.metadata['judge_strategy_note']


def test_ensure_docker_image_builds_missing_default_image(monkeypatch: Any) -> None:
    adapter = make_adapter()
    calls: List[Dict[str, Any]] = []

    def mock_ensure(image: str, path: str, dockerfile: str, label: str) -> bool:
        calls.append({'image': image, 'path': path, 'dockerfile': dockerfile, 'label': label})
        return True

    monkeypatch.setattr('evalscope.benchmarks.gdpval.gdpval_adapter.ensure_docker_image_built', mock_ensure)

    adapter._ensure_docker_image()
    adapter._ensure_docker_image()

    assert calls == [{
        'image': 'evalscope/gdpval:latest',
        'path': str(Path('evalscope/benchmarks/gdpval').resolve()),
        'dockerfile': str(Path('evalscope/benchmarks/gdpval/Dockerfile').resolve()),
        'label': 'GDPval docker image',
    }]


def test_ensure_docker_image_skips_custom_image(monkeypatch: Any) -> None:
    adapter = make_adapter(docker_image='custom/gdpval:latest')

    monkeypatch.setattr(
        'evalscope.benchmarks.gdpval.gdpval_adapter.ensure_docker_image_built',
        lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError(f'unexpected image build: {args} {kwargs}')),
    )

    adapter._ensure_docker_image()


def test_export_submission_writes_parquet_and_copies_deliverables(tmp_path: Path) -> None:
    adapter = make_adapter()
    adapter._submission_records = [{
        'task_id': 'task-1',
        'prompt': 'Create a report.',
        'sector': 'Finance',
        'occupation': 'Analyst',
        'reference_files': [],
        'reference_file_urls': [],
        'reference_file_hf_uris': [],
    }]
    source_file = tmp_path / 'source' / 'report.txt'
    source_file.parent.mkdir()
    source_file.write_text('hello', encoding='utf-8')

    report_dir = tmp_path / 'reports' / 'qwen-plus'
    review_dir = tmp_path / 'reviews' / 'qwen-plus'
    review_dir.mkdir(parents=True)
    review_item = {
        'index': 0,
        'sample_score': {
            'sample_id': 0,
            'sample_metadata': {
                'task_id': 'task-1',
                'deliverable_files': [{
                    'path': 'deliverable_files/report.txt',
                    'local_path': str(source_file),
                }],
            },
            'score': {
                'prediction': 'Done.',
                'extracted_prediction': 'Done.',
            },
        },
    }
    with open(review_dir / 'gdpval_default.jsonl', 'w', encoding='utf-8') as f:
        f.write(json.dumps(review_item) + '\n')

    adapter._export_submission(report_dir)

    submission_dir = report_dir / 'gdpval_submission'
    assert (submission_dir / 'deliverable_files/task-1/report.txt').read_text(encoding='utf-8') == 'hello'
    assert (submission_dir / 'submission_info.json').is_file()

    import pandas as pd
    table = pd.read_parquet(submission_dir / 'data/train-00000-of-00001.parquet')
    assert table.loc[0, 'deliverable_text'] == 'Done.'
    assert table.loc[0, 'deliverable_files'] == ['deliverable_files/task-1/report.txt']


def test_adapter_requires_parquet_dependencies(monkeypatch: Any) -> None:

    def fake_check_import(**kwargs: Any) -> bool:
        assert kwargs['module_name'] == ['pandas', 'pyarrow']
        assert kwargs['raise_error'] is True
        assert kwargs['feature_name'] == 'GDPval submission export'
        raise ImportError('`pyarrow` not found. Please run `pip install pyarrow` to use GDPval submission export.')

    monkeypatch.setattr('evalscope.benchmarks.gdpval.gdpval_adapter.check_import', fake_check_import)

    with pytest.raises(ImportError, match='pyarrow.*GDPval submission export'):
        make_adapter()


class FakeEnvironment:
    name = 'fake'

    def __init__(self, files: Dict[str, bytes]) -> None:
        self.files = files
        self.closed = False

    async def exec(
        self,
        cmd: List[str],
        *,
        cwd: Optional[str] = None,
        input: Optional[str] = None,
        timeout: Optional[float] = None,
        env: Optional[Dict[str, str]] = None,
    ) -> ExecResult:
        if cmd[:3] == ['test', '-d', 'deliverable_files']:
            return ExecResult(returncode=0, stdout='', stderr='')
        if cmd[:4] == ['find', 'deliverable_files', '-type', 'f']:
            return ExecResult(returncode=0, stdout='\0'.join(self.files.keys()) + '\0', stderr='')
        if cmd[:3] == ['base64', '-w', '0']:
            return ExecResult(returncode=0, stdout=base64.b64encode(self.files[cmd[3]]).decode(), stderr='')
        return ExecResult(returncode=1, stdout='', stderr='unexpected command')

    async def close(self) -> None:
        self.closed = True
