"""
Comprehensive tests for GraphQL mutations (Issue #10).

Tests all aspects of the GraphQL write interface including:
- Mutation schema validation
- Markdown file CRUD operations
- Schema CRUD operations
- Error handling
- CLI integration
"""

import pytest
import json
import sqlite3
import tempfile
import os
from datetime import datetime
from pathlib import Path
from unittest.mock import Mock, patch

from markitect.graphql.schema import schema
from markitect.database import DatabaseManager


@pytest.fixture
def temp_db_path():
    """Create temporary database for testing."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name

    # Initialize database with test data
    db_manager = DatabaseManager(db_path)
    db_manager.initialize_database()

    yield db_path

    # Cleanup
    os.unlink(db_path)


@pytest.fixture
def populated_db_path():
    """Create temporary database with some test data."""
    with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
        db_path = f.name

    # Initialize database with test data
    db_manager = DatabaseManager(db_path)
    db_manager.initialize_database()

    # Add sample data
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    # Sample markdown file
    cursor.execute("""
        INSERT INTO markdown_files (filename, content, front_matter, created_at)
        VALUES (?, ?, ?, ?)
    """, (
        'existing.md',
        '# Existing Document\n\nThis document already exists.',
        '{"title": "Existing Document"}',
        datetime.now().isoformat()
    ))

    # Sample schema
    cursor.execute("""
        INSERT INTO schemas (filename, title, description, schema_content, created_at)
        VALUES (?, ?, ?, ?, ?)
    """, (
        'existing-schema.json',
        'Existing Schema',
        'A schema that already exists',
        '{"type": "object", "properties": {"name": {"type": "string"}}}',
        datetime.now().isoformat()
    ))

    conn.commit()
    conn.close()

    yield db_path

    # Cleanup
    os.unlink(db_path)


class TestGraphQLMutationSchema:
    """Test GraphQL mutation schema definition and validation."""

    def test_schema_has_mutations(self):
        """Test that the GraphQL schema has mutations."""
        result = schema.execute('''
        {
          __schema {
            mutationType {
              name
              fields {
                name
                description
              }
            }
          }
        }
        ''')

        assert result.errors is None
        mutation_type = result.data['__schema']['mutationType']
        assert mutation_type is not None
        assert mutation_type['name'] == 'Mutation'

        field_names = [field['name'] for field in mutation_type['fields']]
        assert 'addMarkdownFile' in field_names
        assert 'updateMarkdownFile' in field_names
        assert 'addSchema' in field_names
        assert 'updateSchema' in field_names
        assert 'deleteSchema' in field_names

    def test_add_markdown_file_mutation_signature(self):
        """Test addMarkdownFile mutation has correct signature."""
        result = schema.execute('''
        {
          __schema {
            mutationType {
              fields {
                name
                args {
                  name
                  type {
                    name
                  }
                }
              }
            }
          }
        }
        ''')

        mutation_fields = result.data['__schema']['mutationType']['fields']
        add_file_field = next(f for f in mutation_fields if f['name'] == 'addMarkdownFile')

        arg_names = [arg['name'] for arg in add_file_field['args']]
        assert 'filename' in arg_names
        assert 'content' in arg_names

    def test_mutation_payload_types(self):
        """Test that mutation payload types have correct structure."""
        result = schema.execute('''
        {
          __schema {
            types {
              name
              fields {
                name
                type {
                  name
                }
              }
            }
          }
        }
        ''')

        types = {t['name']: t for t in result.data['__schema']['types']}

        # Check AddMarkdownFilePayload
        payload = types.get('AddMarkdownFilePayload')
        assert payload is not None
        field_names = [f['name'] for f in payload['fields']]
        assert 'markdownFile' in field_names
        assert 'success' in field_names
        assert 'errors' in field_names


class TestMarkdownFileMutations:
    """Test markdown file CRUD mutations."""

    def test_add_markdown_file_success(self, temp_db_path):
        """Test successful markdown file creation."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "new-file.md"
                content: "# New File\\n\\nThis is new content."
              ) {
                success
                markdownFile {
                  id
                  filename
                  content
                  wordCount
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['addMarkdownFile']
            assert data['success'] is True
            assert len(data['errors']) == 0
            assert data['markdownFile'] is not None
            assert data['markdownFile']['filename'] == 'new-file.md'
            assert 'New File' in data['markdownFile']['content']
            assert data['markdownFile']['wordCount'] > 0

    def test_add_markdown_file_with_front_matter(self, temp_db_path):
        """Test markdown file creation with front matter."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            content_with_frontmatter = '''---
title: "Test Document"
author: "Test Author"
tags: ["test", "markdown"]
---

# Test Document

This is a test document with front matter.
'''

            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "with-frontmatter.md"
                content: "%s"
              ) {
                success
                markdownFile {
                  id
                  filename
                  hasFrontMatter
                  frontMatter {
                    key
                    value
                  }
                }
                errors
              }
            }
            ''' % content_with_frontmatter.replace('\n', '\\n').replace('"', '\\"')

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['addMarkdownFile']
            assert data['success'] is True
            assert data['markdownFile']['hasFrontMatter'] is True
            front_matter_keys = [fm['key'] for fm in data['markdownFile']['frontMatter']]
            assert 'title' in front_matter_keys
            assert 'author' in front_matter_keys

    def test_add_markdown_file_duplicate_filename(self, populated_db_path):
        """Test adding a file with duplicate filename (should succeed as update)."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "existing.md"
                content: "# Updated Content\\n\\nThis content replaces the existing."
              ) {
                success
                markdownFile {
                  filename
                  content
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['addMarkdownFile']
            assert data['success'] is True
            assert 'Updated Content' in data['markdownFile']['content']

    def test_update_markdown_file_success(self, populated_db_path):
        """Test successful markdown file update."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
            mutation = '''
            mutation {
              updateMarkdownFile(
                id: 1
                content: "# Updated Title\\n\\nThis content has been updated."
              ) {
                success
                markdownFile {
                  id
                  content
                  wordCount
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['updateMarkdownFile']
            assert data['success'] is True
            assert len(data['errors']) == 0
            assert 'Updated Title' in data['markdownFile']['content']

    def test_update_markdown_file_not_found(self, temp_db_path):
        """Test updating non-existent markdown file."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              updateMarkdownFile(
                id: 999
                content: "# This should fail"
              ) {
                success
                markdownFile {
                  id
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['updateMarkdownFile']
            assert data['success'] is False
            assert data['markdownFile'] is None
            assert len(data['errors']) > 0
            assert 'not found' in data['errors'][0].lower()

    def test_update_markdown_file_no_content(self, populated_db_path):
        """Test updating markdown file without providing content."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
            mutation = '''
            mutation {
              updateMarkdownFile(id: 1) {
                success
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['updateMarkdownFile']
            assert data['success'] is False
            assert 'required' in data['errors'][0].lower()


class TestSchemaMutations:
    """Test JSON schema CRUD mutations."""

    def test_add_schema_success(self, temp_db_path):
        """Test successful schema creation."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            schema_content = {
                "type": "object",
                "title": "User Schema",
                "description": "Schema for user objects",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer", "minimum": 0}
                },
                "required": ["name"]
            }

            mutation = '''
            mutation {
              addSchema(
                filename: "user-schema.json"
                schemaContent: "%s"
              ) {
                success
                schema {
                  id
                  filename
                  title
                  description
                  propertyCount
                }
                errors
              }
            }
            ''' % json.dumps(schema_content).replace('"', '\\"')

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['addSchema']
            assert data['success'] is True
            assert len(data['errors']) == 0
            assert data['schema']['filename'] == 'user-schema.json'
            assert data['schema']['title'] == 'User Schema'
            assert data['schema']['propertyCount'] == 2

    def test_add_schema_invalid_json(self, temp_db_path):
        """Test adding schema with invalid JSON."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              addSchema(
                filename: "invalid-schema.json"
                schemaContent: "{ invalid json }"
              ) {
                success
                schema {
                  id
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            # GraphQL should reject invalid JSON at the schema validation level
            assert result.errors is not None
            assert len(result.errors) > 0
            assert "Badly formed JSONString" in str(result.errors[0])

    def test_update_schema_success(self, populated_db_path):
        """Test successful schema update."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
            new_schema = {
                "type": "object",
                "title": "Updated Schema",
                "properties": {
                    "name": {"type": "string"},
                    "email": {"type": "string", "format": "email"}
                }
            }

            mutation = '''
            mutation {
              updateSchema(
                id: 1
                schemaContent: "%s"
              ) {
                success
                schema {
                  title
                  propertyCount
                }
                errors
              }
            }
            ''' % json.dumps(new_schema).replace('"', '\\"')

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['updateSchema']
            assert data['success'] is True
            assert data['schema']['title'] == 'Updated Schema'
            assert data['schema']['propertyCount'] == 2

    def test_update_schema_not_found(self, temp_db_path):
        """Test updating non-existent schema."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              updateSchema(
                id: 999
                schemaContent: "{\\"type\\": \\"object\\"}"
              ) {
                success
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['updateSchema']
            assert data['success'] is False
            assert 'not found' in data['errors'][0].lower()

    def test_delete_schema_success(self, populated_db_path):
        """Test successful schema deletion."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
            mutation = '''
            mutation {
              deleteSchema(filename: "existing-schema.json") {
                success
                deletedFilename
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['deleteSchema']
            assert data['success'] is True
            assert data['deletedFilename'] == 'existing-schema.json'
            assert len(data['errors']) == 0

    def test_delete_schema_not_found(self, temp_db_path):
        """Test deleting non-existent schema."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              deleteSchema(filename: "nonexistent.json") {
                success
                deletedFilename
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['deleteSchema']
            assert data['success'] is False
            assert data['deletedFilename'] is None


class TestMutationErrorHandling:
    """Test error handling in mutations."""

    def test_database_error_handling(self, temp_db_path):
        """Test mutation behavior when database is unavailable."""
        # Use a non-existent database path
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value='/nonexistent/path.db'):
            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "test.md"
                content: "# Test"
              ) {
                success
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            data = result.data['addMarkdownFile']
            assert data['success'] is False
            assert len(data['errors']) > 0

    def test_invalid_mutation_syntax(self):
        """Test handling of invalid mutation syntax."""
        mutation = '''
        mutation {
          addMarkdownFile(filename: "test.md") {
            success
          }
        }
        '''

        result = schema.execute(mutation)

        # Should have errors due to missing required 'content' argument
        assert result.errors is not None

    def test_missing_required_arguments(self):
        """Test mutations with missing required arguments."""
        mutation = '''
        mutation {
          addSchema(filename: "test.json") {
            success
            errors
          }
        }
        '''

        result = schema.execute(mutation)

        # Should have errors due to missing required 'schemaContent' argument
        assert result.errors is not None


class TestMutationIntegration:
    """Test full integration of mutations with database."""

    def test_crud_workflow(self, temp_db_path):
        """Test complete CRUD workflow for markdown files."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            # 1. Create a file
            create_mutation = '''
            mutation {
              addMarkdownFile(
                filename: "workflow-test.md"
                content: "# Original Content\\n\\nOriginal text."
              ) {
                success
                markdownFile {
                  id
                  filename
                  content
                }
              }
            }
            '''

            result = schema.execute(create_mutation)
            assert result.data['addMarkdownFile']['success'] is True
            file_id = result.data['addMarkdownFile']['markdownFile']['id']

            # 2. Update the file
            update_mutation = '''
            mutation {
              updateMarkdownFile(
                id: %d
                content: "# Updated Content\\n\\nUpdated text."
              ) {
                success
                markdownFile {
                  content
                }
              }
            }
            ''' % file_id

            result = schema.execute(update_mutation)
            assert result.data['updateMarkdownFile']['success'] is True
            assert 'Updated Content' in result.data['updateMarkdownFile']['markdownFile']['content']

    def test_schema_crud_workflow(self, temp_db_path):
        """Test complete CRUD workflow for schemas."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            # 1. Create a schema
            create_mutation = '''
            mutation {
              addSchema(
                filename: "workflow-schema.json"
                schemaContent: "{\\"type\\": \\"object\\", \\"title\\": \\"Original\\"}"
              ) {
                success
                schema {
                  id
                  title
                }
              }
            }
            '''

            result = schema.execute(create_mutation)
            assert result.data['addSchema']['success'] is True
            schema_id = result.data['addSchema']['schema']['id']

            # 2. Update the schema
            update_mutation = '''
            mutation {
              updateSchema(
                id: %d
                schemaContent: "{\\"type\\": \\"object\\", \\"title\\": \\"Updated\\"}"
              ) {
                success
                schema {
                  title
                }
              }
            }
            ''' % schema_id

            result = schema.execute(update_mutation)
            assert result.data['updateSchema']['success'] is True
            assert result.data['updateSchema']['schema']['title'] == 'Updated'

            # 3. Delete the schema
            delete_mutation = '''
            mutation {
              deleteSchema(filename: "workflow-schema.json") {
                success
                deletedFilename
              }
            }
            '''

            result = schema.execute(delete_mutation)
            assert result.data['deleteSchema']['success'] is True
            assert result.data['deleteSchema']['deletedFilename'] == 'workflow-schema.json'


class TestMutationCLI:
    """Test CLI integration for mutations."""

    def test_graphql_mutate_command_available(self):
        """Test that graphql-mutate command is available."""
        import subprocess
        import sys

        result = subprocess.run(
            [sys.executable, "-m", "markitect.cli", "graphql-mutate", "--help"],
            capture_output=True,
            text=True
        )

        assert result.returncode == 0
        assert "Execute GraphQL mutations" in result.stdout
        assert "--local" in result.stdout
        assert "--variables" in result.stdout

    def test_mutation_examples_in_help(self):
        """Test that mutation examples are included in help."""
        import subprocess
        import sys

        result = subprocess.run(
            [sys.executable, "-m", "markitect.cli", "graphql-examples"],
            capture_output=True,
            text=True
        )

        assert result.returncode == 0
        assert "Mutation Examples" in result.stdout
        assert "addMarkdownFile" in result.stdout
        assert "updateMarkdownFile" in result.stdout
        assert "addSchema" in result.stdout
        assert "deleteSchema" in result.stdout


class TestMutationPayloads:
    """Test mutation payload structures."""

    def test_add_markdown_file_payload_structure(self, temp_db_path):
        """Test AddMarkdownFilePayload has correct structure."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "payload-test.md"
                content: "# Payload Test"
              ) {
                success
                markdownFile {
                  id
                  filename
                  content
                  wordCount
                  lineCount
                  hasFrontMatter
                  createdAt
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            payload = result.data['addMarkdownFile']

            # Check payload structure
            assert isinstance(payload['success'], bool)
            assert isinstance(payload['errors'], list)

            if payload['success']:
                md_file = payload['markdownFile']
                assert md_file is not None
                assert isinstance(md_file['id'], int)
                assert isinstance(md_file['filename'], str)
                assert isinstance(md_file['wordCount'], int)
                assert isinstance(md_file['lineCount'], int)
                assert isinstance(md_file['hasFrontMatter'], bool)

    def test_error_payload_structure(self, temp_db_path):
        """Test error payloads have correct structure."""
        with patch('markitect.graphql.resolvers.get_default_database_path', return_value='/nonexistent/path.db'):
            mutation = '''
            mutation {
              addMarkdownFile(
                filename: "error-test.md"
                content: "# Error Test"
              ) {
                success
                markdownFile {
                  id
                }
                errors
              }
            }
            '''

            result = schema.execute(mutation)

            assert result.errors is None
            payload = result.data['addMarkdownFile']

            assert payload['success'] is False
            assert payload['markdownFile'] is None
            assert isinstance(payload['errors'], list)
            assert len(payload['errors']) > 0
            assert all(isinstance(error, str) for error in payload['errors'])