###############################################################################
# (c) Copyright 2020 CERN for the benefit of the LHCb Collaboration           #
#                                                                             #
# This software is distributed under the terms of the GNU General Public      #
# Licence version 3 (GPL Version 3), copied verbatim in the file "COPYING".   #
#                                                                             #
# In applying this licence, CERN does not waive the privileges and immunities #
# granted to it by virtue of its status as an Intergovernmental Organization  #
# or submit itself to any jurisdiction.                                       #
###############################################################################
import json
import os
import shutil
import subprocess
from os.path import dirname, join
from pathlib import Path

import pytest
import yaml

EXAMPLE_REPO_ROOT = join(dirname(__file__), "data", "data-pkg-repo")
B02DKPi_versions = [
    "v0r0p2180615",
    "v0r0p2297458",
    "v0r0p2358939",
    "v0r0p2437974",
    "v0r0p2518507",
    "v0r0p2552694",
    "v0r0p2752886",
    "v0r0p2794507",
    "v0r0p2847846",
    "v0r0p2970193",
    "v0r0p3234056",
    "v0r0p3675801",
]


def pytest_addoption(parser):
    parser.addoption(
        "--runslow", action="store_true", default=False, help="run slow tests"
    )


def pytest_configure(config):
    config.addinivalue_line("markers", "slow: mark test as slow to run")


def pytest_collection_modifyitems(config, items):
    if config.getoption("--runslow"):
        # --runslow given in cli: do not skip slow tests
        return
    skip_slow = pytest.mark.skip(reason="need --runslow option to run")
    for item in items:
        if "slow" in item.keywords:
            item.add_marker(skip_slow)


@pytest.fixture
def empty_data_pkg_repo(tmpdir):
    cwd = os.getcwd()
    os.chdir(tmpdir)
    yield tmpdir
    os.chdir(cwd)


@pytest.fixture
def data_pkg_repo(empty_data_pkg_repo):
    shutil.copytree(
        EXAMPLE_REPO_ROOT, empty_data_pkg_repo, symlinks=True, dirs_exist_ok=True
    )
    yield empty_data_pkg_repo


@pytest.fixture
def with_proxy(mocker):
    from subprocess import check_call as original_check_call

    def my_check_call(args, *more_args, **kwargs):
        if args == ["lb-dirac", "dirac-proxy-info", "--checkvalid"]:
            return
        else:
            return original_check_call(args, *more_args, **kwargs)

    mocker.patch("subprocess.check_call", new=my_check_call)


@pytest.fixture
def without_proxy(mocker):
    mocker.patch(
        "subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "mocked")
    )


@pytest.fixture
def with_data(mocker, monkeypatch):
    from subprocess import run as original_run

    monkeypatch.setenv("LBRUN_BINDS", Path(__file__).parent)

    data_fn1 = (
        Path(__file__).parent / "test-input-dst" / "00070793_00000368_7.AllStreams.dst"
    )
    data_fn2 = (
        Path(__file__).parent / "test-input-dst" / "00070767_00000368_7.AllStreams.dst"
    )

    pool_catalog_xml = (
        '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n'
        "<!-- Edited By PoolXMLCatalog.py -->\n"
        '<!DOCTYPE POOLFILECATALOG SYSTEM "InMemory">\n'
        "<POOLFILECATALOG>\n"
        '<File ID="980B119E-9609-E811-A58E-02163E016452">\n'
        "<physical>\n"
        f'    <pfn filetype="ROOTAll" name="{data_fn1}" se="CI-DST"/>\n'
        "</physical>\n"
        "<logical>\n"
        '    <lfn name="/lhcb/MC/2016/ALLSTREAMS.DST/00070793/0000/00070793_00000368_7.AllStreams.dst"/>\n'
        "</logical>\n"
        "</File>\n"
        '<File ID="043C600A-8F09-E811-B65B-FA163E9FB5C5">\n'
        "<physical>\n"
        f'    <pfn filetype="ROOT_All" name="{data_fn2}" se="CI-DST"/>\n'
        "</physical>\n"
        "<logical>\n"
        '    <lfn name="/lhcb/MC/2016/ALLSTREAMS.DST/00070767/0000/00070767_00000368_7.AllStreams.dst"/>\n'
        "</logical>\n"
        "</File>\n"
        "</POOLFILECATALOG>\n"
    )

    def my_run(args, *more_args, **kwargs):
        if args[:2] == ["lb-dirac", "dirac-bookkeeping-genXMLCatalog"]:
            with open(join(kwargs["cwd"], "pool_xml_catalog.xml"), "wt") as fp:
                fp.write(pool_catalog_xml)
            return subprocess.CompletedProcess([], 0, "")
        else:
            return original_run(args, *more_args, **kwargs)

    mocker.patch("subprocess.run", new=my_run)

    yield pool_catalog_xml


@pytest.fixture(scope="module")
def vcr_config():
    return {
        "filter_headers": ["authorization", "set-cookie"],
        "ignore_hosts": ["auth.cern.ch", "s3.cern.ch"],
    }


def monkeypatch_productions_to_versions(
    working_group: str, production_name: str
) -> list:
    return B02DKPi_versions


@pytest.fixture(autouse=True)
def production_to_versions_patched(monkeypatch):
    import LbAPLocal.cli
    import LbAPLocal.cli.legacy

    monkeypatch.setattr(
        LbAPLocal.cli, "production_to_versions", monkeypatch_productions_to_versions
    )
    monkeypatch.setattr(
        LbAPLocal.cli.legacy,
        "production_to_versions",
        monkeypatch_productions_to_versions,
    )


@pytest.fixture
def fake_global_git_config(tmp_path, monkeypatch):
    """Create a fake global git config file."""
    git_config = tmp_path / "gitconfig"
    lines = [
        "[user]",
        "    name = Test User",
        "    email = test.user@domain.invalid",
    ]
    git_config.write_text("\n".join(lines))
    monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(git_config))


# ============================================================================
# CWL test fixtures
# ============================================================================

from fixture_loader import TEST_DST_DIR, get_fixture  # noqa: E402


@pytest.fixture
def cwl_fixture_set(request):
    """Return FixtureSet for the current test. Override via indirect parametrize."""
    production_name = getattr(request, "param", "executabletests")
    return get_fixture(production_name)


@pytest.fixture
def mock_dirac_calls(monkeypatch, cwl_fixture_set):
    """Mock DIRAC subprocess calls for CWL testing.

    Data-driven from tests/fixtures/ — loads query_dirac results and
    DST file mappings from fixture files.

    Mocks two layers:
    1. LbAPCommon's query_dirac() - returns fixture-loaded results
    2. subprocess.Popen for dirac-cwl-run - pre-generates inputs and replica map,
       strips --force-regenerate and --n-lfns flags so existing files are used
    """
    fixture_set = cwl_fixture_set

    # Verify test DST files exist for all jobs
    for job_name in fixture_set.job_names:
        dst_path = fixture_set.get_dst_path(job_name)
        if not dst_path.is_file():
            pytest.skip(f"Test data file not found: {dst_path}")

    # Bind test data directory into lb-run container
    monkeypatch.setenv("LBRUN_BINDS", str(TEST_DST_DIR))

    # ---- Mock #1: LbAPCommon.__main__.query_dirac ----
    async def fake_query_dirac(
        sem, query_script, name, job_data, output_path, server_credentials
    ):
        """Return fixture-loaded query_dirac results without calling lb-dirac."""
        async with sem:
            result = fixture_set.get_query_result(name)
            output_path.write_text(json.dumps(result))
            return name, job_data, result

    import LbAPCommon.__main__

    monkeypatch.setattr(LbAPCommon.__main__, "query_dirac", fake_query_dirac)

    # ---- Mock #2: subprocess.Popen for dirac-cwl-run ----
    original_popen = subprocess.Popen

    def patched_popen(args, *a, **kw):
        if isinstance(args, list) and args and "dirac-cwl-run" in args[0]:
            # Find the CWL workflow file
            cwl_file = None
            for arg in args:
                if str(arg).endswith(".cwl"):
                    cwl_file = Path(arg)
                    break

            if cwl_file and cwl_file.exists():
                # Match job name from the CWL filename stem
                stem = cwl_file.stem
                matched_job = None
                for job_name in fixture_set.job_names:
                    if job_name in stem:
                        matched_job = job_name
                        break

                if matched_job:
                    lfn = fixture_set.get_lfn(matched_job)
                    local_file = fixture_set.get_dst_path(matched_job)

                    # Write inputs YAML
                    inputs_path = cwl_file.parent / f"{cwl_file.stem}-inputs.yml"
                    inputs_data = {
                        "input-data": [{"class": "File", "location": f"LFN:{lfn}"}]
                    }
                    with open(inputs_path, "w") as f:
                        yaml.dump(
                            inputs_data, f, default_flow_style=False, sort_keys=False
                        )

                    # Write replica map JSON
                    replica_map_path = (
                        cwl_file.parent / f"{cwl_file.stem}-replica-map.json"
                    )
                    replica_map = {
                        lfn: {
                            "replicas": [
                                {
                                    "url": local_file.resolve().as_uri(),
                                    "se": "CI-DST",
                                }
                            ]
                        }
                    }
                    with open(replica_map_path, "w") as f:
                        json.dump(replica_map, f, indent=2)

            # Strip --force-regenerate and --n-lfns from the command
            # so dirac-cwl-run uses the pre-generated files
            new_args = []
            skip_next = False
            for arg in args:
                if skip_next:
                    skip_next = False
                    continue
                if arg == "--force-regenerate":
                    continue
                if arg == "--n-lfns":
                    skip_next = True
                    continue
                if arg == "--pick-smallest-lfn":
                    continue
                new_args.append(arg)
            args = new_args

        return original_popen(args, *a, **kw)

    monkeypatch.setattr(subprocess, "Popen", patched_popen)

    return fixture_set
