from unittest.mock import MagicMock, patch

import pytest

from xfintech.connect.artifact import Artifact
from xfintech.connect.common.connectref import ConnectRef

# ============================================================================
# Fixtures
# ============================================================================


@pytest.fixture
def mock_connect_ref():
    """Create a mock ConnectRef"""
    ref = MagicMock(spec=ConnectRef)
    ref.location = "s3://test-bucket/test-key.json"
    ref.describe = MagicMock(return_value={"location": ref.location})
    ref.to_dict = MagicMock(return_value={"location": ref.location})
    ref.__str__ = MagicMock(return_value=ref.location)
    return ref


@pytest.fixture
def mock_connect():
    """Create a mock Connect object"""
    connect = MagicMock()
    connect.get_object = MagicMock(return_value=b'{"key": "value"}')
    connect.put_object = MagicMock(return_value=MagicMock(location="s3://test-bucket/test-key.json"))
    return connect


@pytest.fixture
def mock_serialiser():
    """Create a mock Serialiser"""
    serialiser = MagicMock()
    serialiser.serialise = MagicMock(return_value=b'{"serialised": true}')
    return serialiser


@pytest.fixture
def mock_deserialiser():
    """Create a mock Deserialiser"""
    deserialiser = MagicMock()
    deserialiser.deserialise = MagicMock(return_value={"deserialised": True})
    return deserialiser


# ============================================================================
# Initialization Tests
# ============================================================================


def test_artifact_init_with_ref_object(mock_connect_ref):
    """Test Artifact initialization with ConnectRef object"""
    artifact = Artifact(ref=mock_connect_ref)

    assert artifact.ref == mock_connect_ref
    assert artifact.data is None
    assert artifact.meta is None


def test_artifact_init_with_ref_dict():
    """Test Artifact initialization with ref as dict"""
    ref_dict = {"location": "s3://test-bucket/test-key.json"}

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_ref = MagicMock()
        mock_from_dict.return_value = mock_ref

        artifact = Artifact(ref=ref_dict)

        mock_from_dict.assert_called_once_with(ref_dict)
        assert artifact.ref == mock_ref


def test_artifact_init_with_data(mock_connect_ref):
    """Test Artifact initialization with data"""
    test_data = {"key": "value", "number": 123}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    assert artifact.ref == mock_connect_ref
    assert artifact.data == test_data
    assert artifact.meta is None


def test_artifact_init_with_meta(mock_connect_ref):
    """Test Artifact initialization with metadata"""
    test_meta = {"author": "test", "version": "1.0"}
    artifact = Artifact(ref=mock_connect_ref, meta=test_meta)

    assert artifact.ref == mock_connect_ref
    assert artifact.data is None
    assert artifact.meta == test_meta
    assert artifact.meta is not test_meta  # Should be a copy


def test_artifact_init_with_all_params(mock_connect_ref):
    """Test Artifact initialization with all parameters"""
    test_data = {"key": "value"}
    test_meta = {"author": "test"}

    artifact = Artifact(ref=mock_connect_ref, data=test_data, meta=test_meta)

    assert artifact.ref == mock_connect_ref
    assert artifact.data == test_data
    assert artifact.meta == test_meta


def test_artifact_init_with_invalid_ref_type():
    """Test Artifact initialization with invalid ref type"""
    with pytest.raises(TypeError, match="Invalid type for ref"):
        Artifact(ref="invalid_string")


def test_artifact_init_with_none_ref():
    """Test Artifact initialization with None ref"""
    with pytest.raises(TypeError):
        Artifact(ref=None)


def test_artifact_meta_is_copied(mock_connect_ref):
    """Test that meta dict is copied, not referenced"""
    original_meta = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, meta=original_meta)

    # Modify original meta
    original_meta["key"] = "changed"

    # Artifact meta should be unchanged
    assert artifact.meta["key"] == "value"


# ============================================================================
# from_dict Tests
# ============================================================================


def test_artifact_from_dict_basic():
    """Test creating Artifact from dict with minimal data"""
    ref_dict = {"location": "s3://test-bucket/test-key.json"}
    artifact_dict = {"ref": ref_dict}

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_ref = MagicMock()
        mock_from_dict.return_value = mock_ref

        artifact = Artifact.from_dict(artifact_dict)

        assert artifact.ref == mock_ref
        assert artifact.data is None
        assert artifact.meta is None


def test_artifact_from_dict_with_data():
    """Test creating Artifact from dict with data"""
    ref_dict = {"location": "s3://test-bucket/test-key.json"}
    test_data = {"key": "value"}
    artifact_dict = {"ref": ref_dict, "data": test_data}

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_ref = MagicMock()
        mock_from_dict.return_value = mock_ref

        artifact = Artifact.from_dict(artifact_dict)

        assert artifact.data == test_data


def test_artifact_from_dict_with_meta():
    """Test creating Artifact from dict with metadata"""
    ref_dict = {"location": "s3://test-bucket/test-key.json"}
    test_meta = {"author": "test"}
    artifact_dict = {"ref": ref_dict, "meta": test_meta}

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_ref = MagicMock()
        mock_from_dict.return_value = mock_ref

        artifact = Artifact.from_dict(artifact_dict)

        assert artifact.meta == test_meta


def test_artifact_from_dict_missing_ref():
    """Test from_dict raises error when ref is missing"""
    artifact_dict = {"data": {"key": "value"}}

    with pytest.raises(KeyError):
        Artifact.from_dict(artifact_dict)


def test_artifact_from_dict_with_all_fields():
    """Test creating Artifact from dict with all fields"""
    ref_dict = {"location": "s3://test-bucket/test-key.json"}
    test_data = {"key": "value"}
    test_meta = {"author": "test", "version": "1.0"}
    artifact_dict = {"ref": ref_dict, "data": test_data, "meta": test_meta}

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_ref = MagicMock()
        mock_from_dict.return_value = mock_ref

        artifact = Artifact.from_dict(artifact_dict)

        assert artifact.ref == mock_ref
        assert artifact.data == test_data
        assert artifact.meta == test_meta


# ============================================================================
# String Representation Tests
# ============================================================================


def test_artifact_str(mock_connect_ref):
    """Test __str__ method"""
    artifact = Artifact(ref=mock_connect_ref)

    result = str(artifact)

    assert result == str(mock_connect_ref)


def test_artifact_repr(mock_connect_ref):
    """Test __repr__ method"""
    artifact = Artifact(ref=mock_connect_ref)

    result = repr(artifact)

    assert "Artifact" in result
    assert str(mock_connect_ref) in result


# ============================================================================
# write Method Tests
# ============================================================================


def test_artifact_write_basic(mock_connect_ref, mock_connect):
    """Test basic write operation without serialisation"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    artifact.write(connect=mock_connect)

    mock_connect.put_object.assert_called_once_with(
        data=test_data,
        location=mock_connect_ref.location,
    )


def test_artifact_write_with_serialiser(mock_connect_ref, mock_connect, mock_serialiser):
    """Test write operation with serialiser"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    artifact.write(connect=mock_connect, serialiser=mock_serialiser, format="json")

    mock_serialiser.serialise.assert_called_once_with(
        test_data,
        format="json",
    )
    mock_connect.put_object.assert_called_once()


def test_artifact_write_no_data(mock_connect_ref, mock_connect):
    """Test write raises error when no data"""
    artifact = Artifact(ref=mock_connect_ref)

    with pytest.raises(ValueError, match="No data to write"):
        artifact.write(connect=mock_connect)


def test_artifact_write_updates_ref(mock_connect_ref, mock_connect):
    """Test write updates the ref with returned value"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    new_ref = MagicMock()
    new_ref.location = "s3://test-bucket/new-key.json"
    mock_connect.put_object.return_value = new_ref

    artifact.write(connect=mock_connect)

    assert artifact.ref == new_ref


def test_artifact_write_with_kwargs(mock_connect_ref, mock_connect):
    """Test write passes through kwargs"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    artifact.write(connect=mock_connect, custom_param="value")

    mock_connect.put_object.assert_called_once_with(
        data=test_data,
        location=mock_connect_ref.location,
        custom_param="value",
    )


def test_artifact_write_serialiser_error(mock_connect_ref, mock_connect, mock_serialiser):
    """Test write handles serialiser errors"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    mock_serialiser.serialise.side_effect = Exception("Serialisation failed")

    with pytest.raises(Exception, match="Failed to write artifact"):
        artifact.write(connect=mock_connect, serialiser=mock_serialiser, format="json")


def test_artifact_write_connect_error(mock_connect_ref, mock_connect):
    """Test write handles connection errors"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    mock_connect.put_object.side_effect = Exception("Connection failed")

    with pytest.raises(Exception, match="Failed to write artifact"):
        artifact.write(connect=mock_connect)


# ============================================================================
# read Method Tests
# ============================================================================


def test_artifact_read_basic(mock_connect_ref, mock_connect):
    """Test basic read operation without deserialisation"""
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.read(connect=mock_connect)

    mock_connect.get_object.assert_called_once_with(
        location=mock_connect_ref.location,
    )
    assert artifact.data == b'{"key": "value"}'
    assert result is artifact  # Should return self


def test_artifact_read_with_deserialiser(mock_connect_ref, mock_connect, mock_deserialiser):
    """Test read operation with deserialiser"""
    artifact = Artifact(ref=mock_connect_ref)

    artifact.read(connect=mock_connect, deserialiser=mock_deserialiser, format="json")

    mock_connect.get_object.assert_called_once()
    mock_deserialiser.deserialise.assert_called_once_with(
        b'{"key": "value"}',
        format="json",
    )
    assert artifact.data == {"deserialised": True}


def test_artifact_read_with_kwargs(mock_connect_ref, mock_connect):
    """Test read passes through kwargs"""
    artifact = Artifact(ref=mock_connect_ref)

    artifact.read(connect=mock_connect, custom_param="value")

    mock_connect.get_object.assert_called_once_with(
        location=mock_connect_ref.location,
        custom_param="value",
    )


def test_artifact_read_returns_self(mock_connect_ref, mock_connect):
    """Test read returns self for chaining"""
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.read(connect=mock_connect)

    assert result is artifact


def test_artifact_read_connect_error(mock_connect_ref, mock_connect):
    """Test read handles connection errors"""
    artifact = Artifact(ref=mock_connect_ref)

    mock_connect.get_object.side_effect = Exception("Connection failed")

    with pytest.raises(Exception, match="Failed to read artifact"):
        artifact.read(connect=mock_connect)


def test_artifact_read_deserialiser_error(mock_connect_ref, mock_connect, mock_deserialiser):
    """Test read handles deserialiser errors"""
    artifact = Artifact(ref=mock_connect_ref)

    mock_deserialiser.deserialise.side_effect = Exception("Deserialisation failed")

    with pytest.raises(Exception, match="Failed to read artifact"):
        artifact.read(connect=mock_connect, deserialiser=mock_deserialiser, format="json")


# ============================================================================
# describe Method Tests
# ============================================================================


def test_artifact_describe_basic(mock_connect_ref):
    """Test describe method with minimal data"""
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.describe()

    assert "ref" in result
    mock_connect_ref.describe.assert_called_once()


def test_artifact_describe_with_meta(mock_connect_ref):
    """Test describe includes metadata"""
    test_meta = {"author": "test", "version": "1.0"}
    artifact = Artifact(ref=mock_connect_ref, meta=test_meta)

    result = artifact.describe()

    assert "ref" in result
    assert "meta" in result
    assert result["meta"] == test_meta


def test_artifact_describe_with_data(mock_connect_ref):
    """Test describe includes data type"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    result = artifact.describe()

    assert "ref" in result
    assert "data_type" in result
    assert result["data_type"] == "dict"


def test_artifact_describe_with_all_fields(mock_connect_ref):
    """Test describe with all fields present"""
    test_data = {"key": "value"}
    test_meta = {"author": "test"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data, meta=test_meta)

    result = artifact.describe()

    assert "ref" in result
    assert "meta" in result
    assert "data_type" in result


def test_artifact_describe_ref_without_method(mock_connect_ref):
    """Test describe handles ref without describe method"""
    mock_connect_ref.describe = None
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.describe()

    assert "ref" in result
    assert isinstance(result["ref"], str)


# ============================================================================
# to_dict Method Tests
# ============================================================================


def test_artifact_to_dict_basic(mock_connect_ref):
    """Test to_dict method with minimal data"""
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.to_dict()

    assert "ref" in result
    mock_connect_ref.to_dict.assert_called_once()


def test_artifact_to_dict_with_meta(mock_connect_ref):
    """Test to_dict includes metadata"""
    test_meta = {"author": "test"}
    artifact = Artifact(ref=mock_connect_ref, meta=test_meta)

    result = artifact.to_dict()

    assert "ref" in result
    assert "meta" in result
    assert result["meta"] == test_meta


def test_artifact_to_dict_with_data(mock_connect_ref):
    """Test to_dict includes data"""
    test_data = {"key": "value"}
    artifact = Artifact(ref=mock_connect_ref, data=test_data)

    result = artifact.to_dict()

    assert "ref" in result
    assert "data" in result
    assert result["data"] == test_data


def test_artifact_to_dict_round_trip(mock_connect_ref):
    """Test from_dict and to_dict round trip"""
    test_data = {"key": "value"}
    test_meta = {"author": "test"}
    artifact1 = Artifact(ref=mock_connect_ref, data=test_data, meta=test_meta)

    # Mock to_dict to return a dict
    mock_connect_ref.to_dict.return_value = {"location": "s3://test-bucket/test-key.json"}

    artifact_dict = artifact1.to_dict()

    with patch("xfintech.connect.artifact.artifact.ConnectRef.from_dict") as mock_from_dict:
        mock_from_dict.return_value = mock_connect_ref

        artifact2 = Artifact.from_dict(artifact_dict)

        assert artifact2.data == test_data
        assert artifact2.meta == test_meta


def test_artifact_to_dict_ref_without_method(mock_connect_ref):
    """Test to_dict handles ref without to_dict method"""
    mock_connect_ref.to_dict = None
    artifact = Artifact(ref=mock_connect_ref)

    result = artifact.to_dict()

    assert "ref" in result
    assert isinstance(result["ref"], str)


# ============================================================================
# Integration Tests
# ============================================================================


def test_artifact_write_read_workflow(mock_connect_ref, mock_connect, mock_serialiser, mock_deserialiser):
    """Test complete write and read workflow"""
    test_data = {"key": "value", "number": 123}

    # Write
    artifact1 = Artifact(ref=mock_connect_ref, data=test_data)
    artifact1.write(connect=mock_connect, serialiser=mock_serialiser, format="json")

    # Read
    artifact2 = Artifact(ref=mock_connect_ref)
    artifact2.read(connect=mock_connect, deserialiser=mock_deserialiser, format="json")

    assert artifact2.data == {"deserialised": True}


def test_artifact_with_metadata_workflow(mock_connect_ref):
    """Test artifact with metadata through its lifecycle"""
    test_data = {"key": "value"}
    test_meta = {"author": "test", "version": "1.0", "created": "2026-01-08"}

    artifact = Artifact(ref=mock_connect_ref, data=test_data, meta=test_meta)

    # Verify metadata is preserved
    description = artifact.describe()
    assert description["meta"] == test_meta

    # Verify metadata in dict representation
    artifact_dict = artifact.to_dict()
    assert artifact_dict["meta"] == test_meta
