"""Unit tests for OdooREST backend — mock HTTP, no external dependencies."""

import json
import time
import urllib.error
import pytest
from unittest.mock import patch, MagicMock

from cli_anything.odoo_12.utils.odoo_rest import OdooREST
from cli_anything.odoo_12.utils.odoo_backend_base import OdooBackend


# ── Helpers ───────────────────────────────────────────────────

def _make_response(data, status=200):
    """Create a mock urllib response."""
    if isinstance(data, bytes):
        body = data
    else:
        body = json.dumps(data).encode("utf-8")
    resp = MagicMock()
    resp.read.return_value = body
    resp.status = status
    resp.__enter__ = MagicMock(return_value=resp)
    resp.__exit__ = MagicMock(return_value=False)
    return resp


def _make_token_response(access_token="test_token", refresh_token="test_refresh",
                         expires_in=3600):
    return _make_response({
        "access_token": access_token,
        "refresh_token": refresh_token,
        "expires_in": expires_in,
        "token_type": "Bearer",
    })


def _authenticated_rest(**kwargs):
    """Create an OdooREST instance with pre-set tokens (skip actual auth)."""
    rpc = OdooREST(
        url="http://localhost:8069",
        db="test",
        user="admin",
        password="admin",
        client_id="test_client",
        client_secret="test_secret",
        access_token="pre_set_token",
        refresh_token="pre_set_refresh",
        token_expiry=time.time() + 3600,
        **kwargs,
    )
    rpc.uid = 1
    return rpc


# ── Constructor & ABC ─────────────────────────────────────────

class TestOdooRESTInit:
    def test_constructor(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin",
                       client_id="cid", client_secret="csecret")
        assert rpc.url == "http://localhost:8069"
        assert rpc.db == "test"
        assert rpc.user == "admin"
        assert rpc.client_id == "cid"
        assert rpc.client_secret == "csecret"
        assert rpc.uid is None

    def test_strips_trailing_slash(self):
        rpc = OdooREST("http://localhost:8069/", "test", "admin", "admin")
        assert rpc.url == "http://localhost:8069"

    def test_is_odoo_backend(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin")
        assert isinstance(rpc, OdooBackend)

    def test_default_token_fields(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin")
        assert rpc.access_token is None
        assert rpc.refresh_token is None
        assert rpc.token_expiry is None


# ── Authentication ────────────────────────────────────────────

class TestOdooRESTAuth:
    @patch("urllib.request.urlopen")
    def test_authenticate_success(self, mock_urlopen):
        token_resp = _make_token_response()
        user_resp = _make_response({"uid": 2, "name": "admin"})
        mock_urlopen.side_effect = [token_resp, user_resp]

        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin",
                       client_id="cid", client_secret="csecret")
        uid = rpc.authenticate()

        assert uid == 2
        assert rpc.access_token == "test_token"
        assert rpc.refresh_token == "test_refresh"
        assert rpc.token_expiry is not None

    def test_authenticate_no_credentials(self):
        rpc = OdooREST("http://localhost:8069", "test", "admin", "admin")
        with pytest.raises(RuntimeError, match="client_id and client_secret"):
            rpc.authenticate()

    @patch("urllib.request.urlopen")
    def test_refresh_token(self, mock_urlopen):
        rpc = _authenticated_rest()
        rpc.token_expiry = time.time() - 100  # expired
        rpc.access_token = "old_token"

        refresh_resp = _make_token_response(access_token="new_token")
        mock_urlopen.return_value = refresh_resp

        rpc._ensure_auth()
        assert rpc.access_token == "new_token"


# ── CRUD mapping ──────────────────────────────────────────────

class TestOdooRESTCrud:
    @patch("urllib.request.urlopen")
    def test_search(self, mock_urlopen):
        mock_urlopen.return_value = _make_response([1, 2, 3])
        rpc = _authenticated_rest()

        result = rpc.search("res.partner", [("is_company", "=", True)], limit=10)
        assert result == [1, 2, 3]

        call_args = mock_urlopen.call_args[0][0]
        assert "/api/search/res.partner" in call_args.full_url
        assert call_args.method == "GET"

    @patch("urllib.request.urlopen")
    def test_search_read(self, mock_urlopen):
        records = [{"id": 1, "name": "Test"}]
        mock_urlopen.return_value = _make_response(records)
        rpc = _authenticated_rest()

        result = rpc.search_read("res.partner", [], fields=["name"], limit=5)
        assert result == records

        call_args = mock_urlopen.call_args[0][0]
        assert "/api/search_read/res.partner" in call_args.full_url

    @patch("urllib.request.urlopen")
    def test_read(self, mock_urlopen):
        records = [{"id": 1, "name": "Test"}]
        mock_urlopen.return_value = _make_response(records)
        rpc = _authenticated_rest()

        result = rpc.read("res.partner", [1], fields=["name"])
        assert result == records

    @patch("urllib.request.urlopen")
    def test_create(self, mock_urlopen):
        mock_urlopen.return_value = _make_response(42)
        rpc = _authenticated_rest()

        result = rpc.create("res.partner", {"name": "New Partner"})
        assert result == 42

        call_args = mock_urlopen.call_args[0][0]
        assert call_args.method == "POST"
        assert "/api/create/res.partner" in call_args.full_url

    @patch("urllib.request.urlopen")
    def test_write(self, mock_urlopen):
        mock_urlopen.return_value = _make_response(True)
        rpc = _authenticated_rest()

        result = rpc.write("res.partner", [1], {"name": "Updated"})
        assert result is True

        call_args = mock_urlopen.call_args[0][0]
        assert call_args.method == "PUT"

    @patch("urllib.request.urlopen")
    def test_unlink(self, mock_urlopen):
        mock_urlopen.return_value = _make_response(True)
        rpc = _authenticated_rest()

        result = rpc.unlink("res.partner", [1])
        assert result is True

        call_args = mock_urlopen.call_args[0][0]
        assert call_args.method == "DELETE"

    @patch("urllib.request.urlopen")
    def test_search_count(self, mock_urlopen):
        mock_urlopen.return_value = _make_response(42)
        rpc = _authenticated_rest()

        result = rpc.search_count("res.partner", [])
        assert result == 42

        call_args = mock_urlopen.call_args[0][0]
        assert "count" in call_args.full_url

    @patch("urllib.request.urlopen")
    def test_fields_get(self, mock_urlopen):
        fields = {"name": {"type": "char", "string": "Name"}}
        mock_urlopen.return_value = _make_response(fields)
        rpc = _authenticated_rest()

        result = rpc.fields_get("res.partner", attributes=["type", "string"])
        assert result == fields


# ── Parameter encoding ────────────────────────────────────────

class TestParamEncoding:
    @patch("urllib.request.urlopen")
    def test_domain_json_encoded(self, mock_urlopen):
        mock_urlopen.return_value = _make_response([])
        rpc = _authenticated_rest()

        rpc.search("res.partner", [("name", "=", "Test")])
        call_args = mock_urlopen.call_args[0][0]
        # Domain should be JSON-encoded in URL
        assert "%5B%5B%22name%22" in call_args.full_url or "domain" in call_args.full_url

    @patch("urllib.request.urlopen")
    def test_fields_json_encoded(self, mock_urlopen):
        mock_urlopen.return_value = _make_response([])
        rpc = _authenticated_rest()

        rpc.search_read("res.partner", [], fields=["name", "email"])
        call_args = mock_urlopen.call_args[0][0]
        assert "fields" in call_args.full_url


# ── Error handling ────────────────────────────────────────────

class TestErrorHandling:
    @patch("urllib.request.urlopen")
    def test_401_retry_with_refresh(self, mock_urlopen):
        """On 401, should refresh token and retry once."""
        error_resp = urllib.error.HTTPError(
            "http://test", 401, "Unauthorized", {}, MagicMock()
        )
        error_resp.read = MagicMock(return_value=b"Unauthorized")

        refresh_resp = _make_token_response(access_token="refreshed_token")
        success_resp = _make_response([1, 2])

        mock_urlopen.side_effect = [error_resp, refresh_resp, success_resp]
        rpc = _authenticated_rest()

        result = rpc.search("res.partner", [])
        assert result == [1, 2]
        assert rpc.access_token == "refreshed_token"

    @patch("urllib.request.urlopen")
    def test_404_error(self, mock_urlopen):
        error_resp = urllib.error.HTTPError(
            "http://test", 404, "Not Found", {}, MagicMock()
        )
        error_resp.read = MagicMock(return_value=b"Not Found")
        mock_urlopen.side_effect = error_resp

        rpc = _authenticated_rest()
        rpc.refresh_token = None  # no refresh to avoid retry

        with pytest.raises(RuntimeError, match="REST API error 404"):
            rpc.search("res.partner", [])

    @patch("urllib.request.urlopen")
    def test_500_error(self, mock_urlopen):
        error_resp = urllib.error.HTTPError(
            "http://test", 500, "Server Error", {}, MagicMock()
        )
        error_resp.read = MagicMock(return_value=b"Internal Server Error")
        mock_urlopen.side_effect = error_resp

        rpc = _authenticated_rest()
        rpc.refresh_token = None

        with pytest.raises(RuntimeError, match="REST API error 500"):
            rpc.search("res.partner", [])

    @patch("urllib.request.urlopen")
    def test_connection_error(self, mock_urlopen):
        mock_urlopen.side_effect = urllib.error.URLError("Connection refused")
        rpc = _authenticated_rest()

        with pytest.raises(RuntimeError, match="Cannot connect"):
            rpc.search("res.partner", [])


# ── execute_kw ────────────────────────────────────────────────

class TestExecuteKw:
    @patch("urllib.request.urlopen")
    def test_execute_kw(self, mock_urlopen):
        mock_urlopen.return_value = _make_response({"result": True})
        rpc = _authenticated_rest()

        rpc.execute_kw("ir.module.module", "button_immediate_install", [[42]])

        call_args = mock_urlopen.call_args[0][0]
        assert "/api/call/ir.module.module/button_immediate_install" in call_args.full_url
        assert call_args.method == "POST"


# ── Report (binary response) ─────────────────────────────────

class TestReportBinary:
    @patch("urllib.request.urlopen")
    def test_render_report(self, mock_urlopen, tmp_path):
        pdf_bytes = b"%PDF-1.4 fake pdf content"
        mock_urlopen.return_value = _make_response(pdf_bytes)
        rpc = _authenticated_rest()

        output_path = str(tmp_path / "report.pdf")
        result = rpc.render_report("account.report_invoice", [1], output_path)

        assert result["report"] == "account.report_invoice"
        assert result["size"] == len(pdf_bytes)
        with open(output_path, "rb") as f:
            assert f.read() == pdf_bytes


# ── Server info ───────────────────────────────────────────────

class TestServerInfo:
    @patch("urllib.request.urlopen")
    def test_server_version(self, mock_urlopen):
        mock_urlopen.return_value = _make_response({"server_version": "12.0"})
        rpc = _authenticated_rest()

        version = rpc.server_version()
        assert "12.0" in str(version)

    @patch("urllib.request.urlopen")
    def test_check_connection(self, mock_urlopen):
        mock_urlopen.return_value = _make_response({"server_version": "12.0"})
        rpc = _authenticated_rest()

        result = rpc.check_connection()
        assert result["url"] == "http://localhost:8069"
        assert result["backend"] == "rest"
