"""
Tests for app/api/dropbox.py module.

Covers Dropbox OAuth endpoints, settings management, and token testing.
"""

from unittest.mock import Mock, patch

import pytest


@pytest.mark.unit
class TestExchangeDropboxToken:
    """Tests for exchange_dropbox_token endpoint."""

    @patch("app.api.dropbox.exchange_oauth_token")
    def test_successful_exchange(self, mock_exchange, client):
        """Test successful OAuth token exchange."""
        mock_exchange.return_value = {
            "refresh_token": "test-refresh",
            "access_token": "test-access",
            "expires_in": 14400,
        }

        response = client.post(
            "/api/dropbox/exchange-token",
            data={
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "redirect_uri": "http://localhost/callback",
                "code": "test-auth-code",
            },
        )

        assert response.status_code == 200
        data = response.json()
        assert data["refresh_token"] == "test-refresh"
        assert data["access_token"] == "test-access"
        assert data["expires_in"] == 14400

    @patch("app.api.dropbox.exchange_oauth_token")
    def test_exchange_default_expiry(self, mock_exchange, client):
        """Test token exchange returns default expires_in when not provided."""
        mock_exchange.return_value = {
            "refresh_token": "test-refresh",
            "access_token": "test-access",
        }

        response = client.post(
            "/api/dropbox/exchange-token",
            data={
                "client_id": "test-client-id",
                "client_secret": "test-client-secret",
                "redirect_uri": "http://localhost/callback",
                "code": "test-auth-code",
            },
        )

        assert response.status_code == 200
        data = response.json()
        assert data["expires_in"] == 14400


@pytest.mark.unit
class TestUpdateDropboxSettings:
    """Tests for update_dropbox_settings endpoint."""

    @patch("app.api.dropbox.settings")
    def test_update_all_settings(self, mock_settings, client):
        """Test updating all Dropbox settings in memory."""
        mock_settings.dropbox_refresh_token = ""
        mock_settings.dropbox_app_key = ""
        mock_settings.dropbox_app_secret = ""
        mock_settings.dropbox_folder = ""

        response = client.post(
            "/api/dropbox/update-settings",
            data={
                "refresh_token": "new-refresh-token",
                "app_key": "new-app-key",
                "app_secret": "new-app-secret",
                "folder_path": "/Documents",
            },
        )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"

    @patch("app.api.dropbox.settings")
    def test_update_refresh_token_only(self, mock_settings, client):
        """Test updating only refresh token."""
        mock_settings.dropbox_refresh_token = ""

        response = client.post(
            "/api/dropbox/update-settings",
            data={
                "refresh_token": "new-refresh-token",
            },
        )

        assert response.status_code == 200

    @patch("app.api.dropbox.settings")
    def test_update_settings_exception(self, mock_settings, client):
        """Test that exceptions return 500 error."""
        # Make setting the attribute raise an exception
        type(mock_settings).dropbox_refresh_token = property(
            lambda self: "",
            lambda self, v: (_ for _ in ()).throw(RuntimeError("forced")),
        )

        response = client.post(
            "/api/dropbox/update-settings",
            data={"refresh_token": "token"},
        )

        assert response.status_code == 500


@pytest.mark.unit
class TestTestDropboxToken:
    """Tests for test_dropbox_token endpoint."""

    @patch("app.api.dropbox.settings")
    def test_not_configured(self, mock_settings, client):
        """Test response when Dropbox is not configured."""
        mock_settings.dropbox_refresh_token = ""
        mock_settings.dropbox_app_key = ""
        mock_settings.dropbox_app_secret = ""

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "error"
        assert "not fully configured" in data["message"]

    @patch("app.api.dropbox.httpx.AsyncClient.post")
    @patch("app.api.dropbox.settings")
    def test_valid_token(self, mock_settings, mock_post, client):
        """Test successful token validation."""
        mock_settings.dropbox_refresh_token = "valid-refresh-token"
        mock_settings.dropbox_app_key = "app-key"
        mock_settings.dropbox_app_secret = "app-secret"
        mock_settings.http_request_timeout = 30

        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "email": "user@example.com",
            "name": {"display_name": "Test User"},
        }
        mock_post.return_value = mock_response

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"
        assert data["account"] == "user@example.com"
        assert data["account_name"] == "Test User"

    @patch("app.api.dropbox.httpx.AsyncClient.post")
    @patch("app.api.dropbox.settings")
    def test_expired_token_refreshed(self, mock_settings, mock_post, client):
        """Test that expired token triggers refresh and retry."""
        mock_settings.dropbox_refresh_token = "refresh-token"
        mock_settings.dropbox_app_key = "app-key"
        mock_settings.dropbox_app_secret = "app-secret"
        mock_settings.http_request_timeout = 30

        # First call returns 401, second call (refresh) returns 200, third call returns 200
        mock_401 = Mock()
        mock_401.status_code = 401

        mock_refresh = Mock()
        mock_refresh.status_code = 200
        mock_refresh.json.return_value = {"access_token": "new-access-token"}

        mock_success = Mock()
        mock_success.status_code = 200
        mock_success.json.return_value = {
            "email": "user@example.com",
            "name": {"display_name": "Test User"},
        }

        mock_post.side_effect = [mock_401, mock_refresh, mock_success]

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"

    @patch("app.api.dropbox.httpx.AsyncClient.post")
    @patch("app.api.dropbox.settings")
    def test_refresh_token_expired(self, mock_settings, mock_post, client):
        """Test handling when refresh token itself is expired."""
        mock_settings.dropbox_refresh_token = "expired-refresh"
        mock_settings.dropbox_app_key = "app-key"
        mock_settings.dropbox_app_secret = "app-secret"
        mock_settings.http_request_timeout = 30

        # First call returns 401, refresh also fails
        mock_401 = Mock()
        mock_401.status_code = 401

        mock_refresh_fail = Mock()
        mock_refresh_fail.status_code = 400
        mock_refresh_fail.text = "invalid_grant"

        mock_post.side_effect = [mock_401, mock_refresh_fail]

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "error"
        assert data["needs_reauth"] is True

    @patch("app.api.dropbox.httpx.AsyncClient.post")
    @patch("app.api.dropbox.settings")
    def test_token_validation_failure(self, mock_settings, mock_post, client):
        """Test handling non-401, non-200 response."""
        mock_settings.dropbox_refresh_token = "token"
        mock_settings.dropbox_app_key = "app-key"
        mock_settings.dropbox_app_secret = "app-secret"
        mock_settings.http_request_timeout = 30

        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"
        mock_post.return_value = mock_response

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "error"

    @patch("app.api.dropbox.httpx.AsyncClient.post")
    @patch("app.api.dropbox.settings")
    def test_connection_error(self, mock_settings, mock_post, client):
        """Test handling of connection exceptions."""
        import httpx

        mock_settings.dropbox_refresh_token = "token"
        mock_settings.dropbox_app_key = "app-key"
        mock_settings.dropbox_app_secret = "app-secret"
        mock_settings.http_request_timeout = 30

        mock_post.side_effect = httpx.RequestError(
            "Connection refused",
            request=httpx.Request("POST", "https://api.dropboxapi.com/2/users/get_current_account"),
        )

        response = client.get("/api/dropbox/test-token")

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "error"
        assert "Connection error" in data["message"]


@pytest.mark.unit
class TestSaveDropboxSettings:
    """Tests for save_dropbox_settings endpoint."""

    @patch("app.api.dropbox.settings")
    def test_save_settings_env_not_found(self, mock_settings, client):
        """Test that missing .env file is non-fatal — DB write still succeeds."""
        with patch("os.path.exists", return_value=False):
            response = client.post(
                "/api/dropbox/save-settings",
                data={"refresh_token": "test-token"},
            )

        # .env write is best-effort; endpoint should still succeed via DB write
        assert response.status_code == 200
        assert response.json()["status"] == "success"

    @patch("app.api.dropbox.settings")
    def test_save_settings_success(self, mock_settings, client, tmp_path):
        """Test successful save of Dropbox settings to .env file."""
        mock_settings.dropbox_refresh_token = ""
        mock_settings.dropbox_app_key = ""
        mock_settings.dropbox_app_secret = ""
        mock_settings.dropbox_folder = ""

        # Create a temporary .env file
        env_file = tmp_path / ".env"
        env_file.write_text("DROPBOX_REFRESH_TOKEN=old_token\nOTHER_VAR=value\n")

        with (
            patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
            patch("app.api.dropbox.os.path.exists", return_value=True),
            patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
        ):
            response = client.post(
                "/api/dropbox/save-settings",
                data={
                    "refresh_token": "new-token",
                    "app_key": "new-key",
                },
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"

        # Verify the .env file was updated
        content = env_file.read_text()
        assert "new-token" in content

    @patch("app.api.dropbox.settings")
    def test_save_settings_with_all_optional_fields(self, mock_settings, client, tmp_path):
        """Test saving all Dropbox settings including optional fields."""
        mock_settings.dropbox_refresh_token = ""
        mock_settings.dropbox_app_key = ""
        mock_settings.dropbox_app_secret = ""
        mock_settings.dropbox_folder = ""

        # Create a temporary .env file with commented lines
        env_file = tmp_path / ".env"
        env_file.write_text(
            "# DROPBOX_REFRESH_TOKEN=old_token\n"
            "# DROPBOX_APP_KEY=old_key\n"
            "# DROPBOX_APP_SECRET=old_secret\n"
            "OTHER_VAR=value\n"
        )

        with (
            patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
            patch("app.api.dropbox.os.path.exists", return_value=True),
            patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
        ):
            response = client.post(
                "/api/dropbox/save-settings",
                data={
                    "refresh_token": "new-token",
                    "app_key": "new-key",
                    "app_secret": "new-secret",
                    "folder_path": "/Documents",
                },
            )

        assert response.status_code == 200
        data = response.json()
        assert data["status"] == "success"

        # Verify all settings were updated in the file
        content = env_file.read_text()
        assert "DROPBOX_REFRESH_TOKEN=new-token" in content
        assert "DROPBOX_APP_KEY=new-key" in content
        assert "DROPBOX_APP_SECRET=new-secret" in content
        assert "DROPBOX_FOLDER=/Documents" in content

    @patch("app.api.dropbox.settings")
    def test_save_settings_adds_missing_keys(self, mock_settings, client, tmp_path):
        """Test that missing keys are added to .env file."""
        mock_settings.dropbox_refresh_token = ""
        mock_settings.dropbox_app_key = ""
        mock_settings.dropbox_app_secret = ""
        mock_settings.dropbox_folder = ""

        # Create a temporary .env file without Dropbox settings
        env_file = tmp_path / ".env"
        env_file.write_text("OTHER_VAR=value\nANOTHER_VAR=test\n")

        with (
            patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
            patch("app.api.dropbox.os.path.exists", return_value=True),
            patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
        ):
            response = client.post(
                "/api/dropbox/save-settings",
                data={
                    "refresh_token": "new-token",
                    "app_key": "new-key",
                    "folder_path": "/Shared",
                },
            )

        assert response.status_code == 200

        # Verify new keys were added
        content = env_file.read_text()
        assert "DROPBOX_REFRESH_TOKEN=new-token" in content
        assert "DROPBOX_APP_KEY=new-key" in content
        assert "DROPBOX_FOLDER=/Shared" in content
        # Original lines should be preserved
        assert "OTHER_VAR=value" in content
        assert "ANOTHER_VAR=test" in content

    @patch("app.api.dropbox.settings")
    def test_save_settings_io_error(self, mock_settings, client, tmp_path):
        """Test that I/O errors on .env write are non-fatal — DB write still succeeds."""
        mock_settings.dropbox_refresh_token = ""

        # Create a temporary .env file
        env_file = tmp_path / ".env"
        env_file.write_text("DROPBOX_REFRESH_TOKEN=old_token\n")

        with (
            patch("app.api.dropbox.os.path.join", return_value=str(env_file)),
            patch("app.api.dropbox.os.path.exists", return_value=True),
            patch("app.api.dropbox.os.path.dirname", return_value=str(tmp_path)),
            patch("builtins.open", side_effect=IOError("Permission denied")),
        ):
            response = client.post(
                "/api/dropbox/save-settings",
                data={"refresh_token": "new-token"},
            )

        # .env write is best-effort; endpoint should still succeed via DB write
        assert response.status_code == 200
        assert response.json()["status"] == "success"


@pytest.mark.unit
class TestListDropboxFolders:
    """Tests for list_dropbox_folders endpoint."""

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_success(self, mock_post, client):
        """Test successful folder listing at root."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "entries": [
                {".tag": "folder", "name": "Documents", "path_display": "/Documents", "id": "id:1"},
                {".tag": "folder", "name": "Photos", "path_display": "/Photos", "id": "id:2"},
                {".tag": "file", "name": "readme.txt", "path_display": "/readme.txt", "id": "id:3"},
            ],
            "has_more": False,
        }
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": ""},
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data["folders"]) == 2
        assert data["folders"][0]["name"] == "Documents"
        assert data["folders"][1]["name"] == "Photos"
        assert data["path"] == "/"
        assert data["has_more"] is False

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_subfolder(self, mock_post, client):
        """Test listing folders in a subfolder."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "entries": [
                {".tag": "folder", "name": "Invoices", "path_display": "/Documents/Invoices", "id": "id:4"},
            ],
            "has_more": False,
        }
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": "/Documents"},
        )

        assert response.status_code == 200
        data = response.json()
        assert len(data["folders"]) == 1
        assert data["folders"][0]["path"] == "/Documents/Invoices"

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_empty(self, mock_post, client):
        """Test listing folders in an empty directory."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"entries": [], "has_more": False}
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": "/EmptyFolder"},
        )

        assert response.status_code == 200
        assert len(response.json()["folders"]) == 0

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_unauthorized(self, mock_post, client):
        """Test listing folders with invalid token returns 401."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.text = "Invalid access token"
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "bad-token", "path": ""},
        )

        assert response.status_code == 401

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_api_error(self, mock_post, client):
        """Test listing folders when Dropbox API returns an error."""
        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.text = "Internal server error"
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": ""},
        )

        assert response.status_code == 502

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_root_path_normalization(self, mock_post, client):
        """Test that '/' is normalized to empty string for Dropbox API."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"entries": [], "has_more": False}
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": "/"},
        )

        assert response.status_code == 200
        # Check the actual API call used empty string for root
        call_args = mock_post.call_args
        assert call_args[1]["json"]["path"] == ""

    @patch("app.api.dropbox.requests.post")
    def test_list_folders_sorted_alphabetically(self, mock_post, client):
        """Test that folders are returned in alphabetical order."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "entries": [
                {".tag": "folder", "name": "Zebra", "path_display": "/Zebra", "id": "id:1"},
                {".tag": "folder", "name": "Alpha", "path_display": "/Alpha", "id": "id:2"},
                {".tag": "folder", "name": "middle", "path_display": "/middle", "id": "id:3"},
            ],
            "has_more": False,
        }
        mock_post.return_value = mock_response

        response = client.post(
            "/api/dropbox/list-folders",
            data={"access_token": "test-token", "path": ""},
        )

        assert response.status_code == 200
        names = [f["name"] for f in response.json()["folders"]]
        assert names == ["Alpha", "middle", "Zebra"]


class TestBuildDropboxRedirectUri:
    """Tests for the _build_dropbox_redirect_uri helper."""

    def test_uses_public_base_url_when_set(self):
        """When PUBLIC_BASE_URL is configured, redirect URI should use it."""
        from unittest.mock import MagicMock

        with patch("app.api.dropbox.settings") as mock_settings:
            mock_settings.public_base_url = "https://myapp.example.com"
            from app.api.dropbox import _build_dropbox_redirect_uri

            mock_request = MagicMock()
            result = _build_dropbox_redirect_uri(mock_request)

        assert result == "https://myapp.example.com/dropbox-callback"

    def test_uses_public_base_url_strips_trailing_slash(self):
        """PUBLIC_BASE_URL with trailing slash should be handled correctly."""
        from unittest.mock import MagicMock

        with patch("app.api.dropbox.settings") as mock_settings:
            mock_settings.public_base_url = "https://myapp.example.com/"
            from app.api.dropbox import _build_dropbox_redirect_uri

            mock_request = MagicMock()
            result = _build_dropbox_redirect_uri(mock_request)

        assert result == "https://myapp.example.com/dropbox-callback"

    def test_falls_back_to_request_when_public_base_url_not_set(self):
        """When PUBLIC_BASE_URL is not set, use request scheme and netloc."""
        from unittest.mock import MagicMock

        with patch("app.api.dropbox.settings") as mock_settings:
            mock_settings.public_base_url = None
            from app.api.dropbox import _build_dropbox_redirect_uri

            mock_request = MagicMock()
            mock_request.url.scheme = "https"
            mock_request.url.netloc = "other.example.com"
            result = _build_dropbox_redirect_uri(mock_request)

        assert result == "https://other.example.com/dropbox-callback"


@pytest.mark.unit
class TestGlobalAuthorizeUrl:
    """Tests for GET /api/dropbox/global-authorize-url endpoint."""

    @patch("app.api.dropbox.settings")
    def test_returns_authorize_url(self, mock_settings, client):
        """Test that a valid authorize URL is returned when global creds are configured."""
        mock_settings.dropbox_allow_global_credentials_for_integrations = True
        mock_settings.dropbox_app_key = "test-app-key"
        mock_settings.dropbox_app_secret = "test-app-secret"
        mock_settings.public_base_url = "https://example.com"

        response = client.get("/api/dropbox/global-authorize-url")

        assert response.status_code == 200
        data = response.json()
        assert "authorize_url" in data
        assert "https://www.dropbox.com/oauth2/authorize" in data["authorize_url"]
        assert "client_id=test-app-key" in data["authorize_url"]
        # redirect_uri should be URL-encoded
        assert "redirect_uri=" in data["authorize_url"]
        assert "https%3A%2F%2Fexample.com%2Fdropbox-callback" in data["authorize_url"]

    @patch("app.api.dropbox.settings")
    def test_returns_403_when_global_creds_disabled(self, mock_settings, client):
        """Test 403 when global credentials for integrations are disabled."""
        mock_settings.dropbox_allow_global_credentials_for_integrations = False
        mock_settings.dropbox_app_key = "test-app-key"
        mock_settings.dropbox_app_secret = "test-app-secret"

        response = client.get("/api/dropbox/global-authorize-url")

        assert response.status_code == 403

    @patch("app.api.dropbox.settings")
    def test_returns_503_when_creds_not_configured(self, mock_settings, client):
        """Test 503 when global Dropbox credentials are not configured."""
        mock_settings.dropbox_allow_global_credentials_for_integrations = True
        mock_settings.dropbox_app_key = None
        mock_settings.dropbox_app_secret = None

        response = client.get("/api/dropbox/global-authorize-url")

        assert response.status_code == 503

    @patch("app.api.dropbox.settings")
    def test_redirect_uri_uses_public_base_url(self, mock_settings, client):
        """Redirect URI in authorize URL must use PUBLIC_BASE_URL when configured."""
        mock_settings.dropbox_allow_global_credentials_for_integrations = True
        mock_settings.dropbox_app_key = "my-key"
        mock_settings.dropbox_app_secret = "my-secret"
        mock_settings.public_base_url = "https://prod.example.com"

        response = client.get("/api/dropbox/global-authorize-url")

        assert response.status_code == 200
        authorize_url = response.json()["authorize_url"]
        # The redirect_uri must be URL-encoded and contain the public base URL
        assert "https%3A%2F%2Fprod.example.com%2Fdropbox-callback" in authorize_url
