import pytest
from unittest.mock import Mock

from geobox.enums import LayerType, RelationshipCardinality
from geobox.exception import NotFoundError, ValidationError
from geobox.table import Table
from geobox.relationship import Relationship, RelationshipEndpoint
from geobox.vectorlayer import VectorLayer


def test_relationship_init(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    assert relationship.uuid == mock_relationship_data['uuid']
    assert relationship.data == mock_relationship_data
    assert relationship.endpoint == f'{Relationship.BASE_ENDPOINT}{relationship.uuid}/'


def test_relationship_repr(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    assert repr(relationship) == f"Relationship(uuid={mock_relationship_data['uuid']}, name={mock_relationship_data['relation_name']}, cardinality={mock_relationship_data['relation_cardinality']})"


def test_relationship_cardinality(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    assert relationship.cardinality == RelationshipCardinality.ManytoMany


def test_get_relationships(api, mock_relationship_data):
    api.get.return_value = [mock_relationship_data, mock_relationship_data]
    relationships = Relationship.get_relationships(api)
    api.get.assert_called_once_with(f'{Relationship.BASE_ENDPOINT}?f=json&return_count=False&skip=0&limit=10&shared=False')
    assert len(relationships) == 2
    assert type(relationships[0]) == Relationship
    assert relationships[0].data == mock_relationship_data



# Mock the required enums and classes (for test_create_relationship only)
class MockRelationshipCardinality:
    OnetoOne = Mock(value="OneToOne")
    OnetoMany = Mock(value="OneToMany")
    ManytoMany = Mock(value="ManyToMany")


class MockTable:
    def __init__(self, id, name, class_name="Table"):
        self.id = id
        self.name = name
        self.__class__.__name__ = class_name


class MockRelationshipEndpoint:
    def __init__(self, table, field, fk_field):
        self.table = table
        self.field = field
        self.fk_field = fk_field


def test_create_relationship(api):
    """
    Test the create_relationship method with all parameters.
    """
    # Create mock table objects
    relation_table = MockTable(id=100, name="book_author_join", class_name="Table")
    source_table = MockTable(id=101, name="books", class_name="Table")
    target_table = MockTable(id=102, name="authors", class_name="Table")
    
    # Create mock relationship endpoints
    # Using string field names
    source_endpoint = MockRelationshipEndpoint(
        table=source_table,
        field="book_id",
        fk_field="book_fk"
    )
    
    target_endpoint = MockRelationshipEndpoint(
        table=target_table,
        field="author_id",
        fk_field="author_fk"
    )
    
    # Mock the cardinality
    cardinality = MockRelationshipCardinality.ManytoMany
    
    # Expected data that should be sent to the API
    expected_data = {
        "relation_name": "book_author_relationship",
        "relation_display_name": "Book-Author Relationship",
        "relation_description": "Links books to their authors",
        "relation_cardinality": "ManyToMany",
        "relation_table_id": 100,
        "relation_table_search": "Table/book_author_join",
        "source_field_name": "book_id",
        "source_fk_name": "book_fk",
        "source_id": 101,
        "source_search": "Table/books",
        "source_type": "Table",
        "target_field_name": "author_id",
        "target_fk_name": "author_fk",
        "target_id": 102,
        "target_search": "Table/authors",
        "target_type": "Table",
        "user_id": 123,
    }
    
    # Mock API response - combine expected data with mock response data
    api_response = {
        "uuid": "new-relationship-uuid-789",
        **expected_data,
        "id": 200,
        "created_at": "2024-01-01T10:00:00Z",
        "updated_at": "2024-01-01T10:00:00Z"
    }
    
    # Set up the mock
    api.post.return_value = api_response
    
    # Call the create_relationship method
    relationship = Relationship.create_relationship(
        api=api,
        name="book_author_relationship",
        cardinality=cardinality,
        relation_table=relation_table,
        source=source_endpoint,
        target=target_endpoint,
        display_name="Book-Author Relationship",
        description="Links books to their authors",
        user_id=123
    )
    
    api.post.assert_called_once_with(
        Relationship.BASE_ENDPOINT,
        expected_data,
    )
    
    # Verify the returned Relationship object
    assert isinstance(relationship, Relationship)
    assert relationship.uuid == "new-relationship-uuid-789"
    assert relationship.data == api_response
    
    # Verify specific attributes
    assert relationship.relation_name == "book_author_relationship"
    assert relationship.relation_cardinality == "ManyToMany"
    
    # Verify the object can be properly represented
    expected_repr = f"Relationship(uuid={relationship.uuid}, name=book_author_relationship, cardinality=ManyToMany)"
    assert repr(relationship) == expected_repr
    
    # Verify endpoint is constructed correctly
    assert relationship.endpoint == f'{Relationship.BASE_ENDPOINT}{relationship.uuid}/'


def test_get_relationship(api, mock_relationship_data):
    api.get.return_value = mock_relationship_data
    relationship = Relationship.get_relationship(api, uuid=mock_relationship_data['uuid'])
    api.get.assert_called_once_with(f"{Relationship.BASE_ENDPOINT}{mock_relationship_data['uuid']}/?f=json")
    assert type(relationship) == Relationship
    assert relationship.uuid == mock_relationship_data['uuid']
    assert relationship.data == mock_relationship_data


def test_get_relationship_by_name(api, mock_relationship_data):
    api.get.return_value = [mock_relationship_data, mock_relationship_data]
    relationship = Relationship.get_relationship_by_name(api, name=mock_relationship_data['relation_name'])
    api.get.assert_called_once_with(f'{Relationship.BASE_ENDPOINT}?f=json&q=name+%3D+%27test1_5%27&return_count=False&skip=0&limit=10&shared=False')
    assert type(relationship) == Relationship
    assert relationship.data == mock_relationship_data

    # not found
    relationship = Relationship.get_relationship_by_name(api, name='not_found')
    assert relationship == None

def test_relationship_update_cardinality(api, mock_relationship_data):
    """Test relationship update with cardinality change."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    updated_data = {
        **mock_relationship_data,
        'relation_cardinality': '1-1',
    }
    api.put.return_value = updated_data

    result = relationship.update(cardinality=RelationshipCardinality.OnetoOne)
    api.put.assert_called_once_with(relationship.endpoint, {'relation_cardinality': '1-1'})
    assert updated_data == result
    assert relationship.data == updated_data


def test_relationship_update_validation_error_many_to_many_without_relation_table(api, mock_relationship_data):
    """Test that ManytoMany cardinality requires relation_table."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)

    with pytest.raises(ValidationError, match="relation_table is required for Many-to-Many relationships"):
        relationship.update(cardinality=RelationshipCardinality.ManytoMany)

    api.put.assert_not_called()


def test_relationship_update_with_source_target_and_relation_table(api, mock_relationship_data, mock_table_data):
    """Test relationship update with source, target, and relation_table."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)

    source_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 19, 'name': 'books'})
    target_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 20, 'name': 'authors'})
    relation_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 21, 'name': 'book_author_join'})

    source_endpoint = RelationshipEndpoint(table=source_table, field='book_id', fk_field='book_fk')
    target_endpoint = RelationshipEndpoint(table=target_table, field='author_id', fk_field='author_fk')

    updated_data = {
        **mock_relationship_data,
        'relation_name': 'book_author',
        'relation_cardinality': 'M-N',
        'relation_table_id': 21,
        'source_id': 19,
        'target_id': 20,
    }
    api.put.return_value = updated_data

    result = relationship.update(
        name='book_author',
        cardinality=RelationshipCardinality.ManytoMany,
        relation_table=relation_table,
        source=source_endpoint,
        target=target_endpoint,
    )

    expected_payload = {
        'relation_name': 'book_author',
        'relation_display_name': None,
        'relation_description': None,
        'relation_cardinality': 'M-N',
        'relation_table_id': 21,
        'relation_table_search': 'Table/book_author_join',
        'source_field_name': 'book_id',
        'source_id': 19,
        'source_search': 'Table/books',
        'source_type': 'Table',
        'source_fk_name': 'book_fk',
        'target_field_name': 'author_id',
        'target_id': 20,
        'target_search': 'Table/authors',
        'target_type': 'Table',
        'target_fk_name': 'author_fk',
    }
    # clean_data removes Nones - verify call with cleaned payload
    cleaned_payload = {k: v for k, v in expected_payload.items() if v is not None}
    api.put.assert_called_once()
    call_args = api.put.call_args[0]
    assert call_args[0] == relationship.endpoint
    assert call_args[1] == cleaned_payload
    assert result == updated_data


def test_relationship_update_with_none_field_covers_get_field_name(api, mock_relationship_data, mock_table_data):
    """Test update with field=None and fk_field=None covers get_field_name return None branch."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)

    source_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 19, 'name': 'books'})
    target_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 20, 'name': 'authors'})
    relation_table = Table(api, mock_table_data['uuid'], {**mock_table_data, 'id': 21, 'name': 'join_table'})

    # field=None and fk_field=None trigger get_field_name return None
    source_endpoint = RelationshipEndpoint(table=source_table, field=None, fk_field=None)
    target_endpoint = RelationshipEndpoint(table=target_table, field=None, fk_field=None)

    updated_data = {**mock_relationship_data, 'relation_name': 'test_rel'}
    api.put.return_value = updated_data

    result = relationship.update(
        name='test_rel',
        cardinality=RelationshipCardinality.ManytoMany,
        relation_table=relation_table,
        source=source_endpoint,
        target=target_endpoint,
    )

    # get_field_name(None) returns None; clean_data removes those keys
    api.put.assert_called_once_with('relationships/6bc880fd-0398-4a17-b1cd-917daa5a4287/', {'relation_name': 'test_rel', 'relation_cardinality': 'M-N', 'relation_table_id': 21, 'relation_table_search': 'Table/join_table', 'source_id': 19, 'source_search': 'Table/books', 'source_type': 'Table', 'target_id': 20, 'target_search': 'Table/authors', 'target_type': 'Table'})
    assert result == updated_data


def test_relationship_update_with_none_table_covers_get_table_info(api, mock_relationship_data):
    """Test update with table=None covers get_table_info return {} branch."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)

    # table=None triggers get_table_info return {}
    source_endpoint = RelationshipEndpoint(table=None, field='some_field', fk_field=None)
    target_endpoint = RelationshipEndpoint(table=None, field='other_field', fk_field=None)

    updated_data = {**mock_relationship_data, 'relation_name': 'test_rel'}
    api.put.return_value = updated_data

    result = relationship.update(
        name='test_rel',
        source=source_endpoint,
        target=target_endpoint,
    )

    # get_table_info(None) returns {} so source_id, source_search, source_type are None
    # clean_data removes them; source_field_name and target_field_name remain
    call_payload = api.put.call_args[0][1]
    assert call_payload.get('source_field_name') == 'some_field'
    assert call_payload.get('target_field_name') == 'other_field'
    assert result == updated_data


def test_relationship_update_partial_display_name_only(api, mock_relationship_data):
    """Test partial update with only display_name."""
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    updated_data = {**mock_relationship_data, 'relation_display_name': 'New Display Name'}
    api.put.return_value = updated_data

    result = relationship.update(display_name='New Display Name')

    api.put.assert_called_once_with(relationship.endpoint, {'relation_display_name': 'New Display Name'})
    assert result == updated_data
    assert relationship.data == updated_data


def test_relationship_delete(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    endpoint = relationship.endpoint
    relationship.delete()
    api.delete.assert_called_once_with(endpoint)
    assert relationship.uuid is None
    assert relationship.endpoint is None


def test_relationship_get_source(api, mock_relationship_data, mock_table_data, mock_vector_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    
    # Test with Table as source (correct, since source_type is 'Table' in fixture)
    mock_table_data['id'] = mock_relationship_data['source_id']
    table = Table(api, mock_table_data['uuid'], mock_table_data)
    api.get_tables.return_value = [table]
    source = relationship.get_source()
    assert source.data == table.data
    assert type(source) == Table

    # Test with VectorLayer as source - need to modify relationship data first
    # Clone the relationship data and change source_type to 'VectorLayer'
    vector_relationship_data = mock_relationship_data.copy()
    vector_relationship_data['source_type'] = 'VectorLayer'
    # Use target_id as source_id for this test
    vector_relationship_data['source_id'] = mock_relationship_data['target_id']
    
    vector_relationship = Relationship(api, vector_relationship_data['uuid'], vector_relationship_data)
    
    mock_vector_data['id'] = vector_relationship_data['source_id']
    layer = VectorLayer(api, 
                        uuid=mock_vector_data['uuid'], 
                        data=mock_vector_data,
                        layer_type=LayerType.MultiPoint)
    api.get_vector.return_value = [layer]  # Note: get_vector (singular), not get_vectors
    
    source = vector_relationship.get_source()
    assert source.data == layer.data
    assert type(source) == VectorLayer

    # error case - test with original relationship (Table as source)
    table.id = 1  # Change ID so it doesn't match
    api.get_tables.return_value = [table]
    with pytest.raises(NotFoundError):
        relationship.get_source()


def test_relationship_get_target(api, mock_relationship_data, mock_table_data, mock_vector_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    
    mock_table_data['id'] = mock_relationship_data['target_id']
    table = Table(api, mock_table_data['uuid'], mock_table_data)
    api.get_tables.return_value = [table]
    target = relationship.get_target()
    assert target.data == table.data
    assert type(target) == Table

    vector_relationship_data = mock_relationship_data.copy()
    vector_relationship_data['target_type'] = 'VectorLayer'
    # Use target_id as source_id for this test
    vector_relationship_data['target_id'] = mock_relationship_data['target_id']
    
    vector_relationship = Relationship(api, vector_relationship_data['uuid'], vector_relationship_data)
    
    mock_vector_data['id'] = vector_relationship_data['target_id']
    layer = VectorLayer(api, 
                        uuid=mock_vector_data['uuid'], 
                        data=mock_vector_data,
                        layer_type=LayerType.MultiPoint)
    api.get_vectors.return_value = [layer]  # Note: get_vector (singular), not get_vectors
    
    target = vector_relationship.get_target()
    assert target.data == layer.data
    assert type(target) == VectorLayer

    # error case - test with original relationship (Table as source)
    table.id = 1  # Change ID so it doesn't match
    api.get_tables.return_value = [table]
    with pytest.raises(NotFoundError):
        relationship.get_target()


def test_get_relation_table(api, mock_relationship_data, mock_table_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    mock_table_data['id'] = mock_relationship_data['relation_table_id']
    table = Table(api, mock_table_data['uuid'], mock_table_data)
    api.get_tables.return_value = [table]
    table = relationship.get_relation_table()
    assert table.data == table.data
    assert type(table) == Table

    # error
    table.id = 1
    api.get_tables.return_value = [table]
    with pytest.raises(NotFoundError):
        relationship.get_relation_table()

    mock_relationship_data['relation_table_id'] = None
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    with pytest.raises(ValueError):
        relationship.get_relation_table()


def test_relationship_associate_records(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    api.post.return_value = {'associated': 2}
    result = relationship.associate_records(
        source_id='1',
        target_ids= [1, 2, 3],
        q="name = 'test'",
    )

    assert result == {'associated': 2}
    api.post.assert_called_once_with(f'{relationship.endpoint}associateRecords/', {'source_id': '1', 'target_ids': '1, 2, 3', 'q': "name = 'test'"}, is_json=False)


def test_relationship_disassociate_records(api, mock_relationship_data):
    relationship = Relationship(api, mock_relationship_data['uuid'], mock_relationship_data)
    api.post.return_value = {'associated': 2}
    result = relationship.disassociate_records(
        source_id='1',
        target_ids= [1, 2, 3],
        q="name = 'test'",
    )

    assert result == {'associated': 2}
    api.post.assert_called_once_with(f'{relationship.endpoint}disassociateRecords/', {'source_id': '1', 'target_ids': '1, 2, 3', 'q': "name = 'test'"}, is_json=False)