from __future__ import annotations

import unittest
from unittest.mock import MagicMock, patch

from apps import aroflo_connector_app as app
from apps.aroflo_connector_app.client import AroFloError


CONTEXT = {
    "request_id": "req-aroflo-test",
    "tenant": {"tenant_id": "tenant-a"},
    "payload": {},
}

API_CREDENTIALS = {
    "aroflo_base_url": "https://api.example.invalid",
    "aroflo_u_encoded": "encoded-user",
    "aroflo_p_encoded": "encoded-password",
    "aroflo_api_secret": "signing-secret",
    "aroflo_org_encoded": "encoded-org",
}


class AroFloContractChecks(unittest.TestCase):
    def test_manifest_is_valid_and_exports_only_initial_actions(self):
        self.assertEqual([], app.APP_MANIFEST.validate())
        self.assertEqual(
            ["bot_status_v1", "bots_v1", "configure_api_v1", "configure_bot_v1", "connect_bot_v1",
             "disconnect_bot_v1", "health_v1", "submit_bot_mfa_v1",
             "test_connection_v1", "users_get_v1", "users_list_v1", "users_search_v1", "zones_v1"],
            sorted(app.APP_MANIFEST.actions),
        )

    @patch("apps.aroflo_connector_app.AroFloWorkerClient")
    @patch("apps.aroflo_connector_app.list_bots")
    def test_bot_status_calls_signed_worker_adapter(self, list_bots, client_class):
        list_bots.return_value = [{"bot_id": "bot_a", "enabled": True}]
        client_class.return_value.call.return_value = {"state": "ONLINE"}
        context = {**CONTEXT, "payload": {"bot_id": "bot_a"}}
        result = app.handle_request(context, "bot_status_v1")
        self.assertEqual("online", result["data"]["state"])
        client_class.return_value.call.assert_called_once_with(
            "GET", "/ui/session/status", tenant_id="tenant-a", bot_id="bot_a",
            payload={}, timeout=15,
        )

    @patch("apps.aroflo_connector_app.list_bots", return_value=[])
    def test_unknown_bot_is_rejected_before_worker_call(self, _list_bots):
        result, status = app.handle_request(
            {**CONTEXT, "payload": {"bot_id": "missing"}}, "connect_bot_v1",
        )
        self.assertEqual(404, status)
        self.assertEqual("bot_not_found", result["error"]["code"])

    @patch("apps.aroflo_connector_app.list_bots", return_value=[{"bot_id": "bot_a", "enabled": True}])
    def test_mfa_requires_code_and_idempotency_key(self, _list_bots):
        result, status = app.handle_request(
            {**CONTEXT, "payload": {"bot_id": "bot_a", "code": "123456"}},
            "submit_bot_mfa_v1",
        )
        self.assertEqual(400, status)
        self.assertEqual("invalid_input", result["error"]["code"])

    @patch("apps.aroflo_connector_app.bot_pool_summary")
    def test_bot_list_returns_only_safe_profiles(self, summary):
        summary.return_value = {"bots": [{"bot_id": "payroll_bot", "label": "Payroll", "zones": ["timesheets"], "enabled": True, "state": "not_connected", "busy": False, "updated_at": "now"}], "configured": 1, "connected": 0, "licensed_limit": 1, "can_add": False}
        result = app.handle_request(CONTEXT, "bots_v1")
        self.assertEqual(1, result["data"]["configured"])
        self.assertNotIn("password", str(result).lower())

    @patch("apps.aroflo_connector_app.configure_bot")
    def test_bot_configuration_does_not_return_credentials(self, configure_bot):
        configure_bot.return_value = {"bot_id": "payroll_bot", "label": "Payroll", "zones": ["timesheets"], "enabled": True, "state": "not_connected", "busy": False, "updated_at": "now"}
        context = {**CONTEXT, "payload": {"bot_id": "payroll_bot", "label": "Payroll", "username": "bot@example.test", "password": "private-value", "zones": ["timesheets"]}}
        result = app.handle_request(context, "configure_bot_v1")
        self.assertEqual("ok", result["status"])
        self.assertNotIn("private-value", str(result))

    @patch("apps.aroflo_connector_app.SecretsManager")
    def test_api_configuration_is_write_only(self, manager_class):
        context = {**CONTEXT, "payload": API_CREDENTIALS}
        result = app.handle_request(context, "configure_api_v1")
        self.assertEqual("ok", result["status"])
        self.assertTrue(result["data"]["configured"])
        self.assertNotIn("signing-secret", str(result))
        self.assertEqual(5, manager_class.return_value.put_secret.call_count)

    def test_zone_catalog_is_local_and_does_not_publish_legacy_operations(self):
        with patch("apps.aroflo_connector_app.AroFloClient") as client_class:
            result = app.handle_request(CONTEXT, "zones_v1")

        self.assertEqual("ok", result["status"])
        self.assertEqual(8, result["data"]["zone_count"])
        self.assertGreater(result["data"]["operation_count"], 0)
        self.assertFalse(result["data"]["provider_checked"])
        self.assertFalse(result["meta"]["execution_enabled"])
        self.assertTrue(all(
            operation["published"] is False
            for zone in result["data"]["zones"]
            for operation in zone["operations"]
        ))
        client_class.assert_not_called()

    @patch("apps.aroflo_connector_app.SecretsManager")
    def test_health_is_local_and_reports_configured_without_provider_call(self, manager_class):
        manager_class.return_value.get_secret.side_effect = (
            lambda **kwargs: API_CREDENTIALS.get(kwargs["secret_name"])
        )

        result = app.handle_request(CONTEXT, "health_v1")

        self.assertEqual("ok", result["status"])
        self.assertTrue(result["data"]["healthy"])
        self.assertTrue(result["data"]["api_credentials_configured"])
        self.assertFalse(result["data"]["provider_checked"])

    @patch("apps.aroflo_connector_app.SecretsManager")
    def test_credentials_are_resolved_only_for_trusted_tenant(self, manager_class):
        manager_class.return_value.get_secret.side_effect = (
            lambda **kwargs: API_CREDENTIALS.get(kwargs["secret_name"])
        )

        app.handle_request(CONTEXT, "health_v1")

        tenant_ids = {
            call.kwargs["tenant_id"]
            for call in manager_class.return_value.get_secret.call_args_list
        }
        self.assertEqual({"tenant-a"}, tenant_ids)

    @patch("apps.aroflo_connector_app.SecretsManager")
    def test_connection_reports_missing_credentials_without_provider_call(self, manager_class):
        manager_class.return_value.get_secret.return_value = None

        with patch("apps.aroflo_connector_app.AroFloClient") as client_class:
            result, status = app.handle_request(CONTEXT, "test_connection_v1")

        self.assertEqual(409, status)
        self.assertEqual("credential_not_configured", result["error"]["code"])
        client_class.assert_not_called()

    @patch("apps.aroflo_connector_app.SecretsManager")
    @patch("apps.aroflo_connector_app.AroFloClient")
    def test_connection_uses_minimal_read_only_probe(self, client_class, manager_class):
        manager_class.return_value.get_secret.side_effect = (
            lambda **kwargs: API_CREDENTIALS.get(kwargs["secret_name"])
        )
        client_class.return_value.request.return_value = {
            "status": "0",
            "statusmessage": "Login OK",
            "zoneresponse": {"lastupdate": []},
        }

        result = app.handle_request(CONTEXT, "test_connection_v1")

        self.assertEqual("ok", result["status"])
        self.assertTrue(result["data"]["connected"])
        self.assertEqual("api", result["data"]["transport"])
        client_class.return_value.request.assert_called_once_with(
            "",
            method="GET",
            params=[("zone", "lastupdate"), ("page", "1")],
            timeout=20,
        )

    @patch("apps.aroflo_connector_app.SecretsManager")
    @patch("apps.aroflo_connector_app.AroFloClient")
    def test_provider_failure_is_safe_and_retryable(self, client_class, manager_class):
        manager_class.return_value.get_secret.side_effect = (
            lambda **kwargs: API_CREDENTIALS.get(kwargs["secret_name"])
        )
        client_class.return_value.request.side_effect = AroFloError(
            "provider payload containing sensitive material"
        )

        result, status = app.handle_request(CONTEXT, "test_connection_v1")

        self.assertEqual(502, status)
        self.assertEqual("provider_connection_failed", result["error"]["code"])
        self.assertTrue(result["error"]["retryable"])
        self.assertNotIn("sensitive material", str(result))

    def test_missing_tenant_context_is_rejected(self):
        result, status = app.handle_request(
            {"request_id": "req-no-tenant", "payload": {"tenant_id": "untrusted"}},
            "test_connection_v1",
        )
        self.assertEqual(400, status)
        self.assertEqual("tenant_context_missing", result["error"]["code"])

    def test_unknown_action_uses_standard_error(self):
        result, status = app.handle_request(CONTEXT, "missing_v1")
        self.assertEqual(404, status)
        self.assertEqual("action_not_found", result["error"]["code"])


if __name__ == "__main__":
    unittest.main()
