# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
"""CLI tests. Shells out to `gh` are stubbed via a fake subprocess.run."""

from __future__ import annotations

import subprocess
from collections.abc import Iterator
from dataclasses import dataclass

import pytest

from github_body_field import cli

CANONICAL_BODY = (
    "### CVE tool link\n"
    "\n"
    "https://cveprocess.apache.org/cve5/CVE-2026-12345\n"
    "\n"
    "### Reporter credited as\n"
    "\n"
    "anonymous\n"
)


@dataclass
class FakeResult:
    returncode: int
    stdout: str = ""
    stderr: str = ""


class FakeGh:
    """Records the (cmd, input) of each subprocess.run and replies with
    canned output. Tests assert on the recorded calls."""

    def __init__(self, body: str = CANONICAL_BODY) -> None:
        self.body = body
        self.calls: list[tuple[list[str], str | None]] = []
        self.next_write_returncode = 0

    def __call__(
        self,
        cmd: list[str],
        *,
        capture_output: bool = False,
        text: bool = False,
        input: str | None = None,
        check: bool = False,
    ) -> FakeResult:
        self.calls.append((cmd, input))
        # Distinguish read vs write by argv shape.
        if "view" in cmd:
            # gh appends a trailing newline to --jq .body output; replicate.
            return FakeResult(returncode=0, stdout=self.body + "\n")
        if "edit" in cmd:
            # Capture the new body so the test can assert on it.
            assert input is not None, "edit must pipe a body via stdin"
            self.body = input  # mimic the round-trip
            return FakeResult(returncode=self.next_write_returncode)
        raise AssertionError(f"unexpected gh invocation: {cmd!r}")


@pytest.fixture
def fake_gh(monkeypatch: pytest.MonkeyPatch) -> Iterator[FakeGh]:
    f = FakeGh()
    monkeypatch.setattr(subprocess, "run", f)
    yield f


# ---------------------------------------------------------------------------
# get
# ---------------------------------------------------------------------------


def test_get_prints_field_value(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(["get", "123", "--field", "Reporter credited as"])
    assert rc == 0
    out = capsys.readouterr().out
    assert out == "anonymous\n"


def test_get_field_missing_exits_3(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(["get", "123", "--field", "Nope"])
    assert rc == 3
    err = capsys.readouterr().err
    assert "not found" in err


def test_get_forwards_repo_to_gh(fake_gh: FakeGh) -> None:
    cli.main(["--repo", "foo/bar", "get", "123", "--field", "Reporter credited as"])
    read_cmd, _ = fake_gh.calls[0]
    assert "--repo" in read_cmd
    assert read_cmd[read_cmd.index("--repo") + 1] == "foo/bar"


# ---------------------------------------------------------------------------
# list
# ---------------------------------------------------------------------------


def test_list_prints_headings_one_per_line(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(["list", "123"])
    assert rc == 0
    out = capsys.readouterr().out
    assert out == "CVE tool link\nReporter credited as\n"


def test_list_json_emits_array(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(["list", "123", "--json"])
    assert rc == 0
    out = capsys.readouterr().out
    assert '"CVE tool link"' in out
    assert '"Reporter credited as"' in out


# ---------------------------------------------------------------------------
# set
# ---------------------------------------------------------------------------


def test_set_pushes_new_body(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(
        [
            "set",
            "123",
            "--field",
            "Reporter credited as",
            "--value",
            "Jane Doe (@jdoe)",
        ]
    )
    assert rc == 0
    # First call: view. Second call: edit with the rewritten body via stdin.
    assert len(fake_gh.calls) == 2
    edit_cmd, edit_input = fake_gh.calls[1]
    assert edit_cmd[:4] == ["gh", "issue", "edit", "123"]
    assert "--body-file" in edit_cmd
    assert "-" in edit_cmd
    assert edit_input is not None
    assert "### Reporter credited as\n\nJane Doe (@jdoe)\n" in edit_input
    # Diff summary on stderr.
    err = capsys.readouterr().err
    assert "field='Reporter credited as'" in err


def test_set_no_op_when_value_unchanged(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    rc = cli.main(
        [
            "set",
            "123",
            "--field",
            "Reporter credited as",
            "--value",
            "anonymous",
        ]
    )
    assert rc == 0
    # Only the view should have happened; the write must be skipped.
    assert len(fake_gh.calls) == 1
    err = capsys.readouterr().err
    assert "unchanged" in err


def test_set_dry_run_does_not_push(fake_gh: FakeGh) -> None:
    rc = cli.main(
        [
            "set",
            "123",
            "--field",
            "Reporter credited as",
            "--value",
            "Jane",
            "--dry-run",
        ]
    )
    assert rc == 0
    # Only the read; no edit call.
    assert len(fake_gh.calls) == 1


def test_set_field_missing_exits_3_without_writing(
    fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]
) -> None:
    rc = cli.main(["set", "123", "--field", "Nope", "--value", "x"])
    assert rc == 3
    # Read happened, edit did not.
    assert len(fake_gh.calls) == 1
    err = capsys.readouterr().err
    assert "not found" in err


def test_set_rejects_both_value_and_value_file(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    with pytest.raises(SystemExit) as exc:
        cli.main(
            [
                "set",
                "123",
                "--field",
                "Reporter credited as",
                "--value",
                "x",
                "--value-file",
                "/tmp/whatever",
            ]
        )
    assert exc.value.code == 2
    err = capsys.readouterr().err
    assert "not both" in err
    # No gh call should have happened.
    assert len(fake_gh.calls) == 0


def test_set_requires_one_of_value_or_value_file(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    with pytest.raises(SystemExit) as exc:
        cli.main(["set", "123", "--field", "Reporter credited as"])
    assert exc.value.code == 2
    err = capsys.readouterr().err
    assert "required" in err


def test_set_value_file_is_read(fake_gh: FakeGh, tmp_path, capsys: pytest.CaptureFixture[str]) -> None:
    value_file = tmp_path / "v.txt"
    value_file.write_text("from-a-file\n", encoding="utf-8")
    rc = cli.main(
        [
            "set",
            "123",
            "--field",
            "Reporter credited as",
            "--value-file",
            str(value_file),
        ]
    )
    assert rc == 0
    edit_input = fake_gh.calls[1][1]
    assert edit_input is not None
    assert "### Reporter credited as\n\nfrom-a-file\n" in edit_input


def test_set_propagates_gh_write_failure(fake_gh: FakeGh, capsys: pytest.CaptureFixture[str]) -> None:
    fake_gh.next_write_returncode = 1
    with pytest.raises(SystemExit) as exc:
        cli.main(
            [
                "set",
                "123",
                "--field",
                "Reporter credited as",
                "--value",
                "Jane",
            ]
        )
    assert exc.value.code == 1
