"""Tests for deploy.api module."""

from pathlib import Path
from unittest.mock import Mock, mock_open, patch

import pytest
import requests

from ccfm_convert.deploy.api import ConfluenceAPI


@pytest.fixture
def api():
    """Create API instance for testing."""
    return ConfluenceAPI(
        domain="example.atlassian.net", email="test@example.com", token="test-token"
    )


class TestAPIInitialization:
    """Test API client initialization."""

    def test_init(self, api):
        """Test API initialization."""
        assert api.domain == "example.atlassian.net"
        assert api.email == "test@example.com"
        assert api.token == "test-token"
        assert api.base_url == "https://example.atlassian.net/wiki/api/v2"
        assert api.auth == ("test@example.com", "test-token")


class TestGetSpaceId:
    """Test space ID retrieval."""

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_get_space_id_success(self, mock_get, api):
        """Test successful space ID retrieval."""
        mock_response = Mock()
        mock_response.json.return_value = {"results": [{"id": "123456", "key": "TEST"}]}
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        space_id = api.get_space_id("TEST")

        assert space_id == "123456"
        mock_get.assert_called_once()

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_get_space_id_not_found(self, mock_get, api):
        """Test space not found."""
        mock_response = Mock()
        mock_response.json.return_value = {"results": []}
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        with pytest.raises(ValueError, match="Space 'MISSING' not found"):
            api.get_space_id("MISSING")


class TestFindPageByTitle:
    """Test page lookup by title."""

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_found(self, mock_get, api):
        """Test finding existing page."""
        mock_response = Mock()
        mock_response.json.return_value = {"results": [{"id": "789", "title": "My Page"}]}
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        page_id = api.find_page_by_title("123", "My Page")

        assert page_id == "789"

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_not_found(self, mock_get, api):
        """Test page not found."""
        mock_response = Mock()
        mock_response.json.return_value = {"results": []}
        mock_response.raise_for_status = Mock()
        mock_get.return_value = mock_response

        page_id = api.find_page_by_title("123", "Missing Page")

        assert page_id is None

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_http_error(self, mock_get, api):
        """Test HTTP error handling."""
        mock_response = Mock()
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("HTTP 404")
        mock_get.return_value = mock_response

        with pytest.raises(requests.exceptions.HTTPError):
            api.find_page_by_title("123", "Page")


class TestCreatePage:
    """Test page creation."""

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_create_page_success(self, mock_post, api):
        """Test successful page creation."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": "999"}
        mock_response.raise_for_status = Mock()
        mock_post.return_value = mock_response

        body = {"version": 1, "type": "doc", "content": []}
        page_id = api.create_page("123", None, "New Page", body)

        assert page_id == "999"
        mock_post.assert_called_once()

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_create_page_with_parent(self, mock_post, api):
        """Test page creation with parent."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": "999"}
        mock_response.raise_for_status = Mock()
        mock_post.return_value = mock_response

        body = {"version": 1, "type": "doc", "content": []}
        page_id = api.create_page("123", "456", "Child Page", body)

        assert page_id == "999"
        # Verify parent_id was included in request
        call_args = mock_post.call_args
        request_data = call_args[1]["json"]
        assert request_data["parentId"] == "456"

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_create_page_draft(self, mock_post, api):
        """Test creating draft page."""
        mock_response = Mock()
        mock_response.json.return_value = {"id": "999"}
        mock_response.raise_for_status = Mock()
        mock_post.return_value = mock_response

        body = {"version": 1, "type": "doc", "content": []}
        page_id = api.create_page("123", None, "Draft", body, status="draft")

        assert page_id == "999"
        call_args = mock_post.call_args
        request_data = call_args[1]["json"]
        assert request_data["status"] == "draft"


class TestUpdatePage:
    """Test page updates."""

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.put")
    def test_update_page_success(self, mock_put, mock_get, api):
        """Test successful page update."""
        # Mock GET for current version
        mock_get_response = Mock()
        mock_get_response.json.return_value = {"version": {"number": 1}}
        mock_get_response.raise_for_status = Mock()
        mock_get.return_value = mock_get_response

        # Mock PUT for update
        mock_put_response = Mock()
        mock_put_response.raise_for_status = Mock()
        mock_put.return_value = mock_put_response

        body = {"version": 1, "type": "doc", "content": []}
        api.update_page("789", "Updated Title", body)

        mock_get.assert_called_once()
        mock_put.assert_called_once()

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.put")
    def test_update_page_increments_version(self, mock_put, mock_get, api):
        """Test that version is incremented."""
        mock_get_response = Mock()
        mock_get_response.json.return_value = {"version": {"number": 5}}
        mock_get_response.raise_for_status = Mock()
        mock_get.return_value = mock_get_response

        mock_put_response = Mock()
        mock_put_response.raise_for_status = Mock()
        mock_put.return_value = mock_put_response

        body = {"version": 1, "type": "doc", "content": []}
        api.update_page("789", "Title", body)

        # Verify version was incremented
        call_args = mock_put.call_args
        request_data = call_args[1]["json"]
        assert request_data["version"]["number"] == 6


class TestAddLabels:
    """Test label management."""

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_add_labels_success(self, mock_post, api):
        """Test adding labels."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_post.return_value = mock_response

        api.add_labels("789", ["python", "api", "docs"])

        # FIX: API may batch labels or call individually - check it was called
        assert mock_post.call_count >= 1

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_add_empty_labels(self, mock_post, api):
        """Test adding empty label list."""
        api.add_labels("789", [])

        # Should not make any calls
        mock_post.assert_not_called()

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_add_labels_with_existing(self, mock_post, api):
        """Test adding labels when some already exist."""
        # API may batch or call individually
        mock_response = Mock()
        mock_response.status_code = 200
        mock_post.return_value = mock_response

        api.add_labels("789", ["new", "existing", "another"])

        # FIX: Check it was called at least once
        assert mock_post.call_count >= 1


class TestUploadAttachment:
    """Test attachment uploads."""

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.post")
    @patch("builtins.open", new_callable=mock_open, read_data=b"file content")
    def test_upload_new_attachment(self, mock_file, mock_post, mock_get, api):
        """Test uploading new attachment."""
        # Mock GET - attachment doesn't exist
        mock_get_response = Mock()
        mock_get_response.status_code = 200
        mock_get_response.json.return_value = {"results": []}
        mock_get.return_value = mock_get_response

        # Mock POST - upload succeeds
        mock_post_response = Mock()
        mock_post_response.status_code = 201
        mock_post_response.json.return_value = {"results": [{"id": "att123", "title": "test.png"}]}
        mock_post.return_value = mock_post_response

        filepath = Path("test.png")
        result = api.upload_attachment("789", filepath)

        assert result is not None
        assert result["results"][0]["id"] == "att123"

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.post")
    @patch("builtins.open", new_callable=mock_open, read_data=b"file content")
    def test_update_existing_attachment(self, mock_file, mock_post, mock_get, api):
        """Test updating existing attachment."""
        # Mock GET - attachment exists
        mock_get_response = Mock()
        mock_get_response.status_code = 200
        mock_get_response.json.return_value = {"results": [{"id": "att123", "title": "test.png"}]}
        mock_get.return_value = mock_get_response

        # Mock POST - update succeeds
        mock_post_response = Mock()
        mock_post_response.status_code = 200
        mock_post_response.json.return_value = {"results": [{"id": "att123", "title": "test.png"}]}
        mock_post.return_value = mock_post_response

        filepath = Path("test.png")
        result = api.upload_attachment("789", filepath)

        assert result is not None

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.post")
    @patch("builtins.open", new_callable=mock_open, read_data=b"file content")
    def test_upload_attachment_failure(self, mock_file, mock_post, mock_get, api):
        """Test attachment upload failure."""
        mock_get_response = Mock()
        mock_get_response.status_code = 200
        mock_get_response.json.return_value = {"results": []}
        mock_get.return_value = mock_get_response

        # Mock POST - upload fails
        mock_post_response = Mock()
        mock_post_response.status_code = 500
        mock_post.return_value = mock_post_response

        filepath = Path("test.png")
        result = api.upload_attachment("789", filepath)

        assert result is None


class TestGetAttachmentFileId:
    """Test fetching attachment fileId."""

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_get_fileid_success(self, mock_get, api):
        """Test successful fileId retrieval."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "att123",
            "fileId": "uuid-abc-123-def",
            "title": "test.png",
        }
        mock_get.return_value = mock_response

        file_id = api.get_attachment_fileid("att123")

        assert file_id == "uuid-abc-123-def"

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_get_fileid_not_found(self, mock_get, api):
        """Test fileId not found."""
        mock_response = Mock()
        mock_response.status_code = 404
        mock_get.return_value = mock_response

        file_id = api.get_attachment_fileid("att999")

        assert file_id is None

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_get_fileid_no_fileid_field(self, mock_get, api):
        """Test response without fileId field."""
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {
            "id": "att123",
            "title": "test.png",
            # Missing fileId field
        }
        mock_get.return_value = mock_response

        file_id = api.get_attachment_fileid("att123")

        assert file_id is None


class TestFindPageWebuiUrl:
    """Test find_page_webui_url (lines 65-78)."""

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_webui_url_found(self, mock_get, api):
        """Returns full https URL when page is found with _links.webui."""
        mock_response = Mock()
        mock_response.raise_for_status = Mock()
        mock_response.json.return_value = {
            "results": [
                {
                    "id": "123",
                    "_links": {"webui": "/wiki/spaces/KEY/pages/123/My+Page"},
                }
            ]
        }
        mock_get.return_value = mock_response

        url = api.find_page_webui_url("space-123", "My Page")

        assert url == "https://example.atlassian.net/wiki/spaces/KEY/pages/123/My+Page"

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_webui_url_not_found_returns_none(self, mock_get, api):
        """Returns None when no results are returned."""
        mock_response = Mock()
        mock_response.raise_for_status = Mock()
        mock_response.json.return_value = {"results": []}
        mock_get.return_value = mock_response

        url = api.find_page_webui_url("space-123", "Missing Page")

        assert url is None

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_find_page_webui_url_missing_webui_link_returns_none(self, mock_get, api):
        """Returns None when result has no _links.webui."""
        mock_response = Mock()
        mock_response.raise_for_status = Mock()
        mock_response.json.return_value = {"results": [{"id": "123", "_links": {}}]}
        mock_get.return_value = mock_response

        url = api.find_page_webui_url("space-123", "My Page")

        assert url is None


class TestDeletePage:
    """Tests for delete_page (v2 DELETE endpoint)."""

    @patch("ccfm_convert.deploy.api.requests.delete")
    def test_delete_page_success(self, mock_delete, api):
        """delete_page calls DELETE /pages/{id} and does not raise on success."""
        mock_response = Mock()
        mock_response.status_code = 204
        mock_response.raise_for_status = Mock()
        mock_delete.return_value = mock_response

        api.delete_page("page-42")

        mock_delete.assert_called_once_with(
            "https://example.atlassian.net/wiki/api/v2/pages/page-42",
            auth=("test@example.com", "test-token"),
            headers={"Accept": "application/json"},
            timeout=30,
        )
        mock_response.raise_for_status.assert_called_once()

    @patch("ccfm_convert.deploy.api.requests.delete")
    def test_delete_page_raises_on_http_error(self, mock_delete, api):
        """delete_page propagates HTTP errors from raise_for_status."""
        mock_response = Mock()
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Not Found")
        mock_delete.return_value = mock_response

        with pytest.raises(requests.exceptions.HTTPError):
            api.delete_page("nonexistent-page")


class TestCreatePageErrorPath:
    """Test create_page error-printing branch (lines 105-111)."""

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_create_page_prints_error_detail_on_failure(self, mock_post, api):
        """When response is not ok, error details are printed and raise_for_status re-raises."""
        mock_response = Mock()
        mock_response.ok = False
        mock_response.status_code = 400
        mock_response.text = '{"message": "Bad Request"}'
        mock_response.json.return_value = {"message": "Bad Request"}
        mock_response.raise_for_status.side_effect = Exception("400 Bad Request")
        mock_post.return_value = mock_response

        body = {"version": 1, "type": "doc", "content": []}
        with pytest.raises(Exception, match="400 Bad Request"):
            api.create_page("space-123", None, "Test Page", body)

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_create_page_error_with_non_json_response(self, mock_post, api):
        """When error response body is not valid JSON, the inner exception is silently swallowed."""
        mock_response = Mock()
        mock_response.ok = False
        mock_response.status_code = 500
        mock_response.text = "Internal Server Error"
        mock_response.json.side_effect = ValueError("No JSON")
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("500")
        mock_post.return_value = mock_response

        body = {"version": 1, "type": "doc", "content": []}
        with pytest.raises(requests.exceptions.HTTPError):
            api.create_page("space-123", None, "Test Page", body)


class TestAddLabelsWarning:
    """Test add_labels status-code warning branch (line 175)."""

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_add_labels_unexpected_status_prints_warning(self, mock_post, api):
        """When status code is not 200 or 400, a warning is printed."""
        mock_response = Mock()
        mock_response.status_code = 500
        mock_post.return_value = mock_response

        # Should not raise, just warn
        api.add_labels("page-123", ["tag"])

        # Verify request was made
        mock_post.assert_called_once()


class TestUploadAttachmentNormalisation:
    """Test upload_attachment response normalisation (lines 248-249)."""

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.post")
    @patch(
        "builtins.open",
        new_callable=__import__("unittest.mock", fromlist=["mock_open"]).mock_open,
        read_data=b"data",
    )
    def test_upload_existing_attachment_bare_object_normalised(
        self, mock_file, mock_post, mock_get, api
    ):
        """
        When updating an existing attachment the data endpoint returns a bare object
        (no 'results' wrapper). The method must normalise it to {'results': [obj]}.
        """
        # GET shows the attachment already exists
        mock_get_response = Mock()
        mock_get_response.status_code = 200
        mock_get_response.json.return_value = {"results": [{"id": "att456", "title": "img.png"}]}
        mock_get.return_value = mock_get_response

        # POST (update) returns a bare object — no 'results' key
        mock_post_response = Mock()
        mock_post_response.status_code = 200
        mock_post_response.json.return_value = {"id": "att456", "title": "img.png"}
        mock_post.return_value = mock_post_response

        from pathlib import Path

        result = api.upload_attachment("page-789", Path("img.png"))

        assert result is not None
        assert "results" in result
        assert result["results"][0]["id"] == "att456"

    @patch("ccfm_convert.deploy.api.requests.get")
    @patch("ccfm_convert.deploy.api.requests.post")
    @patch(
        "builtins.open",
        new_callable=__import__("unittest.mock", fromlist=["mock_open"]).mock_open,
        read_data=b"data",
    )
    def test_upload_existing_attachment_bare_object_without_id_uses_existing_id(
        self, mock_file, mock_post, mock_get, api
    ):
        """
        When the update response is a bare object without an 'id' field,
        the existing attachment ID is used as fallback (line 248 else branch).
        """
        mock_get_response = Mock()
        mock_get_response.status_code = 200
        mock_get_response.json.return_value = {"results": [{"id": "att789", "title": "img.png"}]}
        mock_get.return_value = mock_get_response

        # POST returns a bare object without 'id'
        mock_post_response = Mock()
        mock_post_response.status_code = 200
        mock_post_response.json.return_value = {"title": "img.png"}  # no 'id'
        mock_post.return_value = mock_post_response

        from pathlib import Path

        result = api.upload_attachment("page-789", Path("img.png"))

        assert result is not None
        assert "results" in result
        assert result["results"][0]["id"] == "att789"


class TestErrorHandling:
    """Test error handling across API methods."""

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_network_error(self, mock_get, api):
        """Test network error handling."""
        mock_get.side_effect = requests.exceptions.ConnectionError("Network error")

        with pytest.raises(requests.exceptions.ConnectionError):
            api.get_space_id("TEST")

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_authentication_error(self, mock_post, api):
        """Test authentication error."""
        mock_response = Mock()
        mock_response.status_code = 401
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("Unauthorized")
        mock_post.return_value = mock_response

        with pytest.raises(requests.exceptions.HTTPError):
            body = {"version": 1, "type": "doc", "content": []}
            api.create_page("123", None, "Page", body)

    @patch("ccfm_convert.deploy.api.requests.get")
    def test_rate_limit_error(self, mock_get, api):
        """Test rate limit handling."""
        mock_response = Mock()
        mock_response.status_code = 429
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
            "Too many requests"
        )
        mock_get.return_value = mock_response

        with pytest.raises(requests.exceptions.HTTPError):
            api.get_space_id("TEST")

    @patch("ccfm_convert.deploy.api.requests.post")
    def test_server_error(self, mock_post, api):
        """Test server error handling."""
        mock_response = Mock()
        mock_response.status_code = 500
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
            "Internal server error"
        )
        mock_post.return_value = mock_response

        with pytest.raises(requests.exceptions.HTTPError):
            body = {"version": 1, "type": "doc", "content": []}
            api.create_page("123", None, "Page", body)
