"""Tests for MIDI object-oriented classes."""

import pytest
import time
import coremusic as cm
import coremusic.capi as capi


class TestMIDIClient:
    """Test MIDIClient object-oriented wrapper"""

    def test_midi_client_creation(self):
        """Test MIDIClient creation"""
        client = cm.MIDIClient("Test Client")
        assert isinstance(client, cm.MIDIClient)
        assert isinstance(client, cm.CoreAudioObject)
        assert client.name == "Test Client"
        assert client.object_id != 0
        assert len(client._ports) == 0
        assert not client.is_disposed
        client.dispose()

    def test_midi_client_create_input_port(self):
        """Test MIDIClient input port creation"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_input_port("Test Input Port")
            assert isinstance(port, cm.MIDIInputPort)
            assert isinstance(port, cm.MIDIPort)
            assert isinstance(port, cm.CoreAudioObject)
            assert port.name == "Test Input Port"
            assert port.object_id != 0
            assert port._client is client
            assert len(client._ports) == 1
            assert client._ports[0] is port
        finally:
            client.dispose()

    def test_midi_client_create_output_port(self):
        """Test MIDIClient output port creation"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_output_port("Test Output Port")
            assert isinstance(port, cm.MIDIOutputPort)
            assert isinstance(port, cm.MIDIPort)
            assert port.name == "Test Output Port"
            assert port.object_id != 0
            assert port._client is client
            assert len(client._ports) == 1
            assert client._ports[0] is port
        finally:
            client.dispose()

    def test_midi_client_multiple_ports(self):
        """Test MIDIClient with multiple ports"""
        client = cm.MIDIClient("Multi Port Client")
        try:
            input_port1 = client.create_input_port("Input 1")
            input_port2 = client.create_input_port("Input 2")
            output_port = client.create_output_port("Output 1")
            assert len(client._ports) == 3
            assert input_port1 in client._ports
            assert input_port2 in client._ports
            assert output_port in client._ports
            assert input_port1.object_id != input_port2.object_id
            assert input_port1.object_id != output_port.object_id
            assert input_port2.object_id != output_port.object_id
        finally:
            client.dispose()

    def test_midi_client_disposal(self):
        """Test MIDIClient disposal"""
        client = cm.MIDIClient("Disposal Test Client")
        input_port = client.create_input_port("Test Port")
        assert not client.is_disposed
        assert not input_port.is_disposed
        assert len(client._ports) == 1
        client.dispose()
        assert client.is_disposed
        assert input_port.is_disposed
        assert len(client._ports) == 0

    def test_midi_client_operations_on_disposed_object(self):
        """Test operations on disposed MIDIClient"""
        client = cm.MIDIClient("Test Client")
        client.dispose()
        with pytest.raises(RuntimeError, match="has been disposed"):
            client.create_input_port("Test Port")
        with pytest.raises(RuntimeError, match="has been disposed"):
            client.create_output_port("Test Port")


class TestMIDIPort:
    """Test MIDIPort base class functionality"""

    def test_midi_port_creation(self):
        """Test MIDIPort creation"""
        port = cm.MIDIPort("Test Port")
        assert isinstance(port, cm.MIDIPort)
        assert isinstance(port, cm.CoreAudioObject)
        assert port.name == "Test Port"
        assert port._client is None

    def test_midi_port_disposal(self):
        """Test MIDIPort disposal"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_input_port("Test Port")
            assert not port.is_disposed
            port.dispose()
            assert port.is_disposed
        finally:
            client.dispose()


class TestMIDIInputPort:
    """Test MIDIInputPort functionality"""

    def test_midi_input_port_creation(self):
        """Test MIDIInputPort creation through client"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_input_port("Input Port")
            assert isinstance(port, cm.MIDIInputPort)
            assert isinstance(port, cm.MIDIPort)
            assert port.name == "Input Port"
        finally:
            client.dispose()

    def test_midi_input_port_connect_source(self):
        """Test MIDIInputPort source connection"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_input_port("Input Port")

            class MockEndpoint:
                def __init__(self):
                    self.object_id = 12345

            mock_source = MockEndpoint()
            try:
                port.connect_source(mock_source)
            except cm.MIDIError:
                pass
        finally:
            client.dispose()

    def test_midi_input_port_disconnect_source(self):
        """Test MIDIInputPort source disconnection"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_input_port("Input Port")

            class MockEndpoint:
                def __init__(self):
                    self.object_id = 12345

            mock_source = MockEndpoint()
            try:
                port.disconnect_source(mock_source)
            except cm.MIDIError:
                pass
        finally:
            client.dispose()

    def test_midi_input_port_operations_on_disposed_object(self):
        """Test operations on disposed MIDIInputPort"""
        client = cm.MIDIClient("Test Client")
        port = client.create_input_port("Test Port")
        client.dispose()

        class MockEndpoint:
            def __init__(self):
                self.object_id = 12345

        mock_source = MockEndpoint()
        with pytest.raises(RuntimeError, match="has been disposed"):
            port.connect_source(mock_source)
        with pytest.raises(RuntimeError, match="has been disposed"):
            port.disconnect_source(mock_source)


class TestMIDIOutputPort:
    """Test MIDIOutputPort functionality"""

    def test_midi_output_port_creation(self):
        """Test MIDIOutputPort creation through client"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_output_port("Output Port")
            assert isinstance(port, cm.MIDIOutputPort)
            assert isinstance(port, cm.MIDIPort)
            assert port.name == "Output Port"
        finally:
            client.dispose()

    def test_midi_output_port_send_data(self):
        """Test MIDIOutputPort data sending"""
        client = cm.MIDIClient("Test Client")
        try:
            port = client.create_output_port("Output Port")

            class MockEndpoint:
                def __init__(self):
                    self.object_id = 54321

            mock_destination = MockEndpoint()
            test_data = b"\x90@\x7f"
            try:
                port.send_data(mock_destination, test_data)
                port.send_data(mock_destination, test_data, timestamp=1000)
            except cm.MIDIError:
                pass
        finally:
            client.dispose()

    def test_midi_output_port_operations_on_disposed_object(self):
        """Test operations on disposed MIDIOutputPort"""
        client = cm.MIDIClient("Test Client")
        port = client.create_output_port("Test Port")
        client.dispose()

        class MockEndpoint:
            def __init__(self):
                self.object_id = 54321

        mock_destination = MockEndpoint()
        test_data = b"\x90@\x7f"
        with pytest.raises(RuntimeError, match="has been disposed"):
            port.send_data(mock_destination, test_data)


class TestMIDIIntegration:
    """Integration tests for MIDI functionality"""

    def test_midi_vs_functional_api_consistency(self):
        """Test MIDI OO API vs functional API consistency"""
        func_client_id = capi.midi_client_create("Functional Client")
        try:
            func_input_port_id = capi.midi_input_port_create(
                func_client_id, "Functional Input"
            )
            func_output_port_id = capi.midi_output_port_create(
                func_client_id, "Functional Output"
            )
            capi.midi_port_dispose(func_input_port_id)
            capi.midi_port_dispose(func_output_port_id)
        finally:
            capi.midi_client_dispose(func_client_id)
        oo_client = cm.MIDIClient("OO Client")
        try:
            oo_input_port = oo_client.create_input_port("OO Input")
            oo_output_port = oo_client.create_output_port("OO Output")
            assert func_client_id != 0
            assert func_input_port_id != 0
            assert func_output_port_id != 0
            assert oo_client.object_id != 0
            assert oo_input_port.object_id != 0
            assert oo_output_port.object_id != 0
        finally:
            oo_client.dispose()

    def test_midi_client_full_workflow(self):
        """Test complete MIDI workflow"""
        client = cm.MIDIClient("Workflow Client")
        try:
            input_port = client.create_input_port("Workflow Input")
            output_port = client.create_output_port("Workflow Output")
            assert len(client._ports) == 2

            class MockEndpoint:
                def __init__(self, id_val):
                    self.object_id = id_val

            mock_source = MockEndpoint(11111)
            mock_destination = MockEndpoint(22222)
            try:
                input_port.connect_source(mock_source)
                output_port.send_data(mock_destination, b"\x90@\x7f")
                input_port.disconnect_source(mock_source)
            except cm.MIDIError:
                pass
        finally:
            client.dispose()

    def test_midi_multiple_clients(self):
        """Test multiple MIDI clients"""
        client1 = cm.MIDIClient("Client 1")
        client2 = cm.MIDIClient("Client 2")
        try:
            assert client1.object_id != client2.object_id
            assert client1.name != client2.name
            port1 = client1.create_input_port("Port 1")
            port2 = client2.create_output_port("Port 2")
            assert port1._client is client1
            assert port2._client is client2
            assert port1.object_id != port2.object_id
        finally:
            client1.dispose()
            client2.dispose()

    def test_midi_resource_management(self):
        """Test MIDI resource management"""
        for i in range(3):
            client = cm.MIDIClient(f"Resource Test Client {i}")
            for j in range(2):
                client.create_input_port(f"Input {j}")
                client.create_output_port(f"Output {j}")
            assert len(client._ports) == 4
            client.dispose()
            assert client.is_disposed

    def test_midi_error_handling(self):
        """Test MIDI error handling"""
        try:
            empty_client = cm.MIDIClient("")
            empty_client.dispose()
        except cm.MIDIError:
            pass
        clients = []
        try:
            for i in range(10):
                client = cm.MIDIClient(f"Stress Test Client {i}")
                clients.append(client)
        except cm.MIDIError:
            pass
        finally:
            for client in clients:
                if not client.is_disposed:
                    client.dispose()


class TestMIDIPortPolymorphism:
    """Test MIDI port polymorphism and inheritance"""

    def test_midi_port_inheritance(self):
        """Test MIDI port class inheritance"""
        client = cm.MIDIClient("Inheritance Test")
        try:
            input_port = client.create_input_port("Input")
            output_port = client.create_output_port("Output")
            assert isinstance(input_port, cm.MIDIInputPort)
            assert isinstance(input_port, cm.MIDIPort)
            assert isinstance(input_port, cm.CoreAudioObject)
            assert isinstance(output_port, cm.MIDIOutputPort)
            assert isinstance(output_port, cm.MIDIPort)
            assert isinstance(output_port, cm.CoreAudioObject)
            ports = [input_port, output_port]
            for port in ports:
                assert hasattr(port, "name")
                assert hasattr(port, "dispose")
                assert hasattr(port, "is_disposed")
        finally:
            client.dispose()

    def test_midi_port_list_management(self):
        """Test MIDI client port list management"""
        client = cm.MIDIClient("List Test")
        try:
            port1 = client.create_input_port("Input 1")
            port2 = client.create_output_port("Output 1")
            port3 = client.create_input_port("Input 2")
            assert len(client._ports) == 3
            assert all(isinstance(port, cm.MIDIPort) for port in client._ports)
            port2.dispose()
            assert port2.is_disposed
            assert len(client._ports) == 2
        finally:
            client.dispose()
