from __future__ import annotations

import json
import os
import subprocess
from types import SimpleNamespace
from unittest.mock import patch

import pytest

from py_azpe.azcli_backend import AzCliBackend, AzCliError


def _make_completed_process(stdout: str = "{}", stderr: str = ""):
    return SimpleNamespace(stdout=stdout, stderr=stderr)


@patch("py_azpe.azcli_backend.subprocess.run")
def test_list_policy_definitions_subscription(mock_run):
    # simulate az rest returning a list with one policy definition
    resp = {"value": [{"name": "p1", "properties": {"displayName": "P1"}}]}
    mock_run.return_value = _make_completed_process(json.dumps(resp), "")

    b = AzCliBackend(subscription_id="0000")
    res = b.list_policy_definitions()

    assert isinstance(res, list)
    assert len(res) == 1
    assert res[0]["name"] == "p1"
    # assert subprocess.run was called with az and rest and subscription id in URI
    called_cmd = mock_run.call_args[0][0]
    assert called_cmd[0] == "az"
    assert "rest" in called_cmd
    # find the uri arg
    uri_items = [
        c for c in called_cmd if isinstance(c, str) and "management.azure.com" in c
    ]
    assert uri_items, "no management.azure.com URI found in az command"
    assert "/subscriptions/0000/" in uri_items[0]
    assert "policyDefinitions" in uri_items[0]


@patch("py_azpe.azcli_backend.subprocess.run")
def test_create_policy_definition_writes_body_and_parses_response(mock_run, tmp_path):
    # simulate az rest returning the created policy definition
    resp = {"name": "p2", "properties": {"displayName": "P2"}}
    mock_run.return_value = _make_completed_process(json.dumps(resp), "")

    b = AzCliBackend(subscription_id="0000")
    body = {"name": "p2", "properties": {"displayName": "P2"}}
    res = b.create_or_update_policy_definition("p2", body)

    assert isinstance(res, dict)
    assert res.get("name") == "p2"

    # ensure subprocess was called; inspect args to find temp file created with @ prefix
    called_cmd = mock_run.call_args[0][0]
    # the --body value will be passed as a separate element like '@/tmp/tmpXXXXX.json'
    body_items = [c for c in called_cmd if isinstance(c, str) and c.startswith("@")]
    assert body_items, "expected a @tmpfile argument in az call"
    tmpfile = body_items[0][1:]
    # temporary file should have been removed by the backend
    assert not os.path.exists(tmpfile)


@patch("py_azpe.azcli_backend.subprocess.run")
def test_create_and_delete_assignment(mock_run):
    # We'll simulate two separate subprocess calls: one for PUT (create) and one for DELETE
    create_resp = {"name": "assign1", "properties": {"displayName": "Assign 1"}}
    delete_resp = {}

    # configure side_effect to return different results per call
    mock_run.side_effect = [
        _make_completed_process(json.dumps(create_resp), ""),
        _make_completed_process(json.dumps(delete_resp), ""),
    ]

    b = AzCliBackend(subscription_id="0000")
    assignment = {
        "name": "assign1",
        "properties": {"displayName": "Assign 1", "scope": "/subscriptions/0000"},
    }

    # create
    res_create = b.create_or_update_assignment(
        "assign1", assignment, scope="/subscriptions/0000"
    )
    assert isinstance(res_create, dict)
    assert res_create.get("name") == "assign1"

    # inspect the first call (PUT)
    first_cmd = mock_run.call_args_list[0][0][0]
    uri_items = [
        c for c in first_cmd if isinstance(c, str) and "management.azure.com" in c
    ]
    assert uri_items and "policyAssignments" in uri_items[0]
    assert "/subscriptions/0000" in uri_items[0]
    # find temp body file
    body_items = [c for c in first_cmd if isinstance(c, str) and c.startswith("@")]
    assert body_items
    tmpfile = body_items[0][1:]
    # ensure temp file has been removed by backend
    assert not os.path.exists(tmpfile)

    # delete
    res_del = b.delete_assignment("assign1", scope="/subscriptions/0000")
    assert res_del == {}

    # inspect second call (DELETE)
    second_cmd = mock_run.call_args_list[1][0][0]
    uri_items2 = [
        c for c in second_cmd if isinstance(c, str) and "management.azure.com" in c
    ]
    assert uri_items2 and "policyAssignments" in uri_items2[0]
    assert "/subscriptions/0000" in uri_items2[0]


# New tests: error handling and tenant-scoped listing
@patch("py_azpe.azcli_backend.subprocess.run")
def test_azcli_raises_on_calledprocesserror(mock_run):
    # Simulate az failing with a CalledProcessError
    mock_run.side_effect = subprocess.CalledProcessError(
        returncode=2, cmd=["az", "rest"], stderr="boom error"
    )

    b = AzCliBackend(subscription_id="0000")
    with pytest.raises(AzCliError) as exc:
        b.list_policy_definitions()
    assert "boom error" in str(exc.value)


@patch("py_azpe.azcli_backend.subprocess.run")
def test_non_json_stdout_returns_raw(mock_run):
    # Simulate az returning non-JSON stdout
    mock_run.return_value = _make_completed_process("OK", "")

    b = AzCliBackend(subscription_id="0000")
    # call internal runner directly to observe behavior
    res = b._run_az(
        [
            "rest",
            "--method",
            "GET",
            "--uri",
            "https://management.azure.com/providers/Microsoft.Authorization/policyDefinitions?api-version=2021-06-01",
        ]
    )
    assert res == {"raw": "OK"}


@patch("py_azpe.azcli_backend.subprocess.run")
def test_list_policy_definitions_tenant_scope(mock_run):
    # simulate tenant-scoped list when no subscription id is provided
    resp = {"value": [{"name": "t1", "properties": {"displayName": "T1"}}]}
    mock_run.return_value = _make_completed_process(json.dumps(resp), "")

    b = AzCliBackend(subscription_id=None)
    res = b.list_policy_definitions()

    assert isinstance(res, list) and len(res) == 1
    assert res[0]["name"] == "t1"
    called_cmd = mock_run.call_args[0][0]
    uri_items = [
        c for c in called_cmd if isinstance(c, str) and "management.azure.com" in c
    ]
    assert uri_items
    # tenant-scoped should not include /subscriptions/
    assert "/subscriptions/" not in uri_items[0]
    assert "policyDefinitions" in uri_items[0]


if __name__ == "__main__":
    pytest.main([__file__])
