"""Unit tests for functions/APIFunctions.py.

External collaborators (APILogger/OTLP, S3Client, AthenzClient, DB handlers) are
mocked so these stay pure unit tests with no network / disk / service access.
"""

import json
from unittest.mock import MagicMock

import pytest

import functions.APIFunctions as apimod
from functions.APIFunctions import APIFunctions
from tests.conftest import FakeRequest

pytestmark = pytest.mark.unit


class FakeParser:
    def __init__(self, values):
        self._v = values

    def add_argument(self, *a, **k):
        pass

    def parse_args(self):
        return self._v


class FakeReqparse:
    def __init__(self, values):
        self._v = values

    def RequestParser(self):
        return FakeParser(self._v)


@pytest.fixture
def api(mocker):
    mocker.patch.object(apimod, "APILogger", return_value=MagicMock())
    instance = APIFunctions(env="prod")
    instance.set_env("prod")
    return instance


# ── env handling ─────────────────────────────────────────────────────────────
def test_set_env_dev(api):
    api.set_env("dev")
    assert api.env == "dev"
    assert api.access_key == "test-dev-access-key"


def test_set_env_prod_variants(api):
    for env in ("prod", "prod_secret", "prod_top_secret", "ssk_prod"):
        api.set_env(env)
        assert api.access_key == "test-access-key"


def test_set_env_invalid(api):
    with pytest.raises(ValueError):
        api.set_env("nope")


# ── small helpers ────────────────────────────────────────────────────────────
def test_cal_to_mega(api):
    assert api._cal_to_mega(2_000_000) == 2.0


def test_check_fos_no_bucket(api):
    ok, resp = api._check_fos_path_and_permission(None)
    assert ok is False and resp["code"] == 400


def test_check_fos_empty_object(api):
    ok, resp = api._check_fos_path_and_permission("b", "")
    assert ok is False and resp["code"] == 400


def test_check_fos_no_keys(api):
    api.access_key = None
    ok, resp = api._check_fos_path_and_permission("b", "o")
    assert ok is False and resp["code"] == 403


def test_check_fos_ok(api):
    ok, resp = api._check_fos_path_and_permission("b", "o")
    assert ok is True and resp is None


# ── make_common_response ─────────────────────────────────────────────────────
def test_make_common_response_no_request(api):
    out = api.make_common_response(request=None, res_msg={"msg": "hi"}, res_code=201)
    assert out["status"] == 201 and out["body"] == {"msg": "hi"}


def test_make_common_response_with_request(api):
    req = FakeRequest(method="POST", path="/foo", host="h", headers={"X-Forwarded-For": "1.2.3.4"})
    body, code = api.make_common_response(request=req)
    payload = json.loads(body)
    assert code == 200
    assert payload["method"] == "POST" and payload["path"] == "/foo"
    assert payload["client_info"]["client_ip"] == "1.2.3.4"


def test_get_client_ip_fallback_header(api):
    # No X-Forwarded-For and an "unknown" remote_addr → falls through to the
    # secondary proxy headers.
    req = FakeRequest(headers={"Proxy-Client-IP": "9.9.9.9"}, remote_addr="unknown")
    assert api._get_client_ip(req) == "9.9.9.9"


def test_get_client_ip_remote_addr(api):
    req = FakeRequest(headers={}, remote_addr="5.5.5.5")
    assert api._get_client_ip(req) == "5.5.5.5"


def test_get_server_info(api, mocker):
    mocker.patch("functions.APIFunctions.socket.gethostname", return_value="host1")
    mocker.patch("functions.APIFunctions.socket.gethostbyname", return_value="10.0.0.1")
    assert api._get_server_info() == ("host1", "10.0.0.1")


def test_get_server_info_resolution_failure(api, mocker):
    mocker.patch("functions.APIFunctions.socket.gethostname", return_value="host1")
    mocker.patch("functions.APIFunctions.socket.gethostbyname", side_effect=OSError("no dns"))
    assert api._get_server_info() == ("host1", "unknown")


# ── run_cmd / run_iperf ──────────────────────────────────────────────────────
def test_run_cmd_empty(api):
    req = FakeRequest()
    _, code = api.run_cmd(FakeReqparse({"cmd": ""}), req)
    assert code == 400


def test_run_cmd_success(api, mocker):
    req = FakeRequest()
    fake_pipe = MagicMock()
    fake_pipe.read.return_value = "output"
    mocker.patch("functions.APIFunctions.os.popen", return_value=fake_pipe)
    body, code = api.run_cmd(FakeReqparse({"cmd": "echo hi"}), req)
    assert code == 200 and "output" in json.loads(body)["msg"]


def test_run_iperf_not_installed(api, mocker):
    mocker.patch.object(api, "_is_iperf_installed", return_value=False)
    _, code = api.run_iperf(FakeReqparse({}), FakeRequest())
    assert code == 500


def test_run_iperf_bad_protocol(api, mocker):
    mocker.patch.object(api, "_is_iperf_installed", return_value=True)
    params = {"host_ip": "1.1.1.1", "host_port": "5201", "duration": 1, "protocol": "icmp"}
    _, code = api.run_iperf(FakeReqparse(params), FakeRequest())
    assert code == 400


def test_is_iperf_installed(api, mocker):
    pipe = MagicMock()
    pipe.read.return_value = "iperf 3.9"
    mocker.patch("functions.APIFunctions.os.popen", return_value=pipe)
    assert api._is_iperf_installed() is True


# ── render_markdown error branch ─────────────────────────────────────────────
def test_render_markdown_missing_file(api):
    _, code = api.render_markdown(FakeRequest(), "definitely-missing-file.md")
    assert code == 404


def test_render_markdown_non_markdown(api):
    _, code = api.render_markdown(FakeRequest(), "notes.txt")
    assert code == 404


# ── DB routing ───────────────────────────────────────────────────────────────
@pytest.mark.parametrize(
    "product,attr",
    [
        ("mysql", "mysql_handler"),
        ("postgresql", "postgresql_handler"),
        ("mongodb", "mongodb_handler"),
        ("redis", "redis_handler"),
        ("cassandra", "cassandra_handler"),
        ("opensearch", "opensearch_handler"),
    ],
)
def test_connect_to_db_routes(api, product, attr):
    getattr(api, attr).connect = MagicMock(return_value={"code": 200})
    assert api.connect_to_db(product, "svc", "u", "p")["code"] == 200


def test_connect_to_db_invalid(api):
    assert api.connect_to_db("bogus", "svc")["code"] == 400


def test_execute_query_routes(api):
    api.mysql_handler.execute_query = MagicMock(return_value={"code": 200})
    assert api.execute_query("mysql", "SELECT 1")["code"] == 200


def test_execute_query_invalid(api):
    assert api.execute_query("bogus", "q")["code"] == 400


def test_execute_opensearch_api_query_connect_fail(api, mocker):
    handler = MagicMock()
    handler.connect.return_value = {"code": 500}
    mocker.patch.object(apimod, "OpenSearchHandler", return_value=handler)
    assert api.execute_opensearch_api_query("s", "u", "p", "GET", "/x")["code"] == 500


def test_execute_opensearch_api_query_success(api, mocker):
    handler = MagicMock()
    handler.connect.return_value = {"code": 200}
    handler.execute_api_query.return_value = {"code": 200, "msg": "ok"}
    mocker.patch.object(apimod, "OpenSearchHandler", return_value=handler)
    assert api.execute_opensearch_api_query("s", "u", "p", "GET", "/x")["code"] == 200


# ── Athenz access token ──────────────────────────────────────────────────────
def test_get_athenz_access_token_success_and_cache(api, mocker):
    client = MagicMock()
    client.get_access_token.return_value = "tok"
    ctor = mocker.patch.object(apimod, "AthenzClient", return_value=client)
    req = FakeRequest(json_body={"account_name": "a", "provider_domain": "p"})
    rp = FakeReqparse({"account_name": "a", "provider_domain": "p"})
    api.get_athenz_access_token(rp, req)
    api.get_athenz_access_token(rp, req)  # cached → ctor only called once
    assert ctor.call_count == 1


def test_get_athenz_access_token_error(api, mocker):
    mocker.patch.object(apimod, "AthenzClient", side_effect=RuntimeError("athenz"))
    req = FakeRequest(json_body={"account_name": "a", "provider_domain": "p"})
    rp = FakeReqparse({"account_name": "a", "provider_domain": "p"})
    _, code = api.get_athenz_access_token(rp, req)
    assert code == 500


# ── FOS object operations ────────────────────────────────────────────────────
def test_get_fos_object_permission_fail(api):
    api.access_key = None
    resp = api.get_fos_object(FakeRequest(), "b", "o")
    assert resp["code"] == 403


def test_get_fos_object_success(api, mocker):
    s3 = MagicMock()
    body = MagicMock()
    body.read.return_value = b"hello"
    s3.get_object.return_value = (True, {"ResponseMetadata": {"HTTPStatusCode": 200}, "Body": body})
    mocker.patch.object(apimod, "S3Client", return_value=s3)
    resp = api.get_fos_object(FakeRequest(), "b", "o")
    assert resp["code"] == 200 and "hello" in resp["msg"]


def test_download_object_bad_args(api, mocker):
    mocker.patch.object(apimod, "S3Client", return_value=MagicMock())
    req = FakeRequest(args={"env": "prod"})
    _, code = api.download_object(req, "b", "")
    assert code == 400


def test_update_bucket_invalid_action(api, mocker):
    mocker.patch.object(apimod, "S3Client", return_value=MagicMock())
    req = FakeRequest(json_body={"env": "prod"})
    assert api.update_bucket(req, "b", "frobnicate")["code"] == 400


def test_update_bucket_success(api, mocker):
    s3 = MagicMock()
    s3.update_bucket.return_value = (True, {"ok": 1})
    mocker.patch.object(apimod, "S3Client", return_value=s3)
    req = FakeRequest(json_body={"env": "prod"})
    assert api.update_bucket(req, "b", "enable")["code"] == 200


def test_get_list_object_success(api, mocker):
    s3 = MagicMock()
    s3.get_list_object.return_value = (True, [{"Key": "a"}, {"Key": "b"}])
    mocker.patch.object(apimod, "S3Client", return_value=s3)
    req = FakeRequest(args={"env": "prod"})
    resp = api.get_list_object(req, "b")
    assert resp["code"] == 200 and "a" in resp["msg"]


# ── dual-env operations ──────────────────────────────────────────────────────
def test_connect_to_dual_mysql_no_body(api):
    assert api.connect_to_dual_mysql(FakeRequest(json_body=None))["code"] == 400


def test_connect_to_dual_mysql_missing_fields(api):
    body = {"ssk": {"service": "s"}, "kks": {}}
    out = api.connect_to_dual_mysql(FakeRequest(json_body=body))
    assert out["overall_success"] is False
    assert out["environments"]["ssk"]["code"] == 400


def test_connect_to_dual_mysql_success(api):
    api.mysql_handler.connect = MagicMock(return_value={"code": 200, "msg": "ok"})
    body = {
        "ssk": {"service": "s", "user": "u", "password": "p"},
        "kks": {"service": "s2", "user": "u2", "password": "p2"},
    }
    out = api.connect_to_dual_mysql(FakeRequest(json_body=body))
    assert out["overall_success"] is True and out["code"] == 200


def test_execute_dual_mysql_no_body(api):
    assert api.execute_dual_mysql_query(FakeRequest(json_body=None))["code"] == 400


def test_execute_dual_mysql_missing_query(api):
    body = {"ssk": {}, "kks": {}}
    assert api.execute_dual_mysql_query(FakeRequest(json_body=body))["code"] == 400


def test_write_to_dual_env_permission_fail(api):
    api.access_key = None
    req = FakeRequest(json_body={"data": "x"})
    rp = FakeReqparse({"data": "x"})
    out = api.write_to_dual_env(rp, req, "b", "o")
    assert out["overall_success"] is False


def test_list_from_dual_env_permission_fail(api):
    api.access_key = None
    out = api.list_from_dual_env(FakeRequest(), "b")
    assert out["overall_success"] is False
