# tests/test_experiment_builder.py
import pytest
from cerillo.cdcl import (
    ExperimentBuilder,
    PlateTemplateBuilder,
    FullExperimentBuilder,
    ExperimentType,
    PlateTreatmentType,
    WellBottomType,
    PlateLidType
)
from cerillo.experiment_results import (
    ExperimentResults, DataTransmissionParser)


class TestExperimentBuilder:
    """Test suite for ExperimentBuilder"""

    def test_simple_build(self):
        """Test building a simple experiment"""
        builder = ExperimentBuilder()
        commands = builder.set_interval(60).build()

        assert len(commands) == 3
        assert commands[1].raw_message == "A i 60"

    def test_chained_commands(self):
        """Test chaining multiple commands"""
        builder = ExperimentBuilder()
        commands = (builder
                    .reset()
                    .set_interval(60)
                    .set_duration(3600)
                    .set_name("Test")
                    .build())

        assert len(commands) == 5
        assert commands[0].raw_message == "A 0"
        assert commands[1].raw_message == "A i 60"
        assert commands[2].raw_message == "A d 3600"
        assert commands[3].raw_message == "A n Test"
        assert commands[4].raw_message == "A ."

    def test_experiment_types(self):
        """Test setting different experiment types"""
        builder = ExperimentBuilder()

        # Kinetic
        commands = builder.set_type(ExperimentType.KINETIC).build()
        assert commands[1].raw_message == "A t 1"

        builder.clear()

        # Endpoint
        commands = builder.set_type(ExperimentType.ENDPOINT).build()
        assert commands[1].raw_message == "A t 0"

    def test_queue_command(self):
        """Test queue command"""
        builder = ExperimentBuilder()

        # Queue = True
        commands = builder.set_queue(True).build()
        assert commands[1].raw_message == "A q 1"

        builder.clear()

        # Queue = False
        commands = builder.set_queue(False).build()
        assert commands[1].raw_message == "A q 0"

    def test_with_command_id(self):
        """Test using command IDs"""
        builder = ExperimentBuilder(command_id="test")
        commands = builder.set_interval(60).build()

        assert len(commands) == 3
        assert commands[0].command_id == "test"
        assert commands[1].raw_message == "A*test i 60"

    def test_build_messages(self):
        """Test build_messages returns raw strings"""
        builder = ExperimentBuilder()
        messages = (builder
                    .set_interval(60)
                    .set_duration(3600)
                    .build_messages())

        assert len(messages) == 4
        assert isinstance(messages[0], str)
        assert messages[0] == 'A 0'
        assert messages[1] == "A i 60"
        assert messages[2] == "A d 3600"
        assert messages[3] == 'A .'

    def test_clear(self):
        """Test clearing builder"""
        builder = ExperimentBuilder()
        builder.set_interval(60).set_duration(3600)

        assert len(builder.build()) == 4

        builder.clear()
        assert len(builder.build()) == 2

    def test_user_command(self):
        """Test setting user"""
        builder = ExperimentBuilder()
        commands = builder.set_user("researcher@lab.com").build()

        assert commands[1].raw_message == "A U researcher@lab.com"

    def test_experiment_id(self):
        """Test setting experiment ID"""
        builder = ExperimentBuilder()
        commands = builder.set_experiment_id("uuid-1234").build()

        assert commands[1].raw_message == "A I uuid-1234"


class TestPlateTemplateBuilder:
    """Test suite for PlateTemplateBuilder"""

    def test_96_well_plate(self):
        """Test 96-well plate preset"""
        builder = PlateTemplateBuilder()
        commands = builder.set_96_well_plate().build()

        # Should set 5 parameters (rows, cols, diameter, height, volume)
        assert len(commands) == 5

        messages = [cmd.raw_message for cmd in commands]
        assert "A PMR 8" in messages
        assert "A PMC 12" in messages

    def test_6_well_plate(self):
        """Test 6-well plate preset"""
        builder = PlateTemplateBuilder()
        commands = builder.set_6_well_plate().build()

        messages = [cmd.raw_message for cmd in commands]
        assert "A PMR 2" in messages
        assert "A PMC 3" in messages

    def test_custom_dimensions(self):
        """Test setting custom dimensions"""
        builder = PlateTemplateBuilder()
        commands = (builder
                    .set_rows(4)
                    .set_cols(6)
                    .set_well_diameter(10000)
                    .build())

        assert len(commands) == 3
        assert commands[0].raw_message == "A PMR 4"
        assert commands[1].raw_message == "A PMC 6"
        assert commands[2].raw_message == "A PMD 10000"

    def test_treatment_types(self):
        """Test setting plate treatment"""
        builder = PlateTemplateBuilder()

        # Tissue culture treated
        commands = builder.set_tissue_culture_treated().build()
        assert commands[0].raw_message == "A PMT t"

        builder.clear()

        # Untreated
        commands = builder.set_untreated().build()
        assert commands[0].raw_message == "A PMT u"

    def test_bottom_types(self):
        """Test setting well bottom types"""
        builder = PlateTemplateBuilder()

        # Flat bottom
        commands = builder.set_flat_bottom().build()
        assert commands[0].raw_message == "A PMB f"

        builder.clear()

        # Round bottom
        commands = builder.set_round_bottom().build()
        assert commands[0].raw_message == "A PMB r"

    def test_lid_types(self):
        """Test setting lid types"""
        builder = PlateTemplateBuilder()

        # Standard lid
        commands = builder.set_standard_lid().build()
        assert commands[0].raw_message == "A PML l"

        builder.clear()

        # No lid
        commands = builder.set_no_lid().build()
        assert commands[0].raw_message == "A PML n"

        builder.clear()

        # Breathe-Easy
        commands = builder.set_breathe_easy().build()
        assert commands[0].raw_message == "A PML m"

    def test_detector_assignment(self):
        """Test assigning detectors to wells"""
        builder = PlateTemplateBuilder()
        commands = builder.assign_detector_to_well(1, 0).build()

        assert len(commands) == 1
        assert commands[0].raw_message == "A PMW 1 0"

    def test_metadata(self):
        """Test setting plate metadata"""
        builder = PlateTemplateBuilder()
        commands = (builder
                    .set_manufacturer("Corning")
                    .set_model("3595")
                    .set_template_name("Test Plate")
                    .build())

        assert len(commands) == 3
        assert commands[0].raw_message == "A PMM Corning"
        assert commands[1].raw_message == "A PMm 3595"
        assert commands[2].raw_message == "A PMn Test Plate"

    def test_complete_plate_config(self):
        """Test complete plate configuration"""
        builder = PlateTemplateBuilder()
        messages = (builder
                    .set_96_well_plate()
                    .set_manufacturer("Corning")
                    .set_model("3595")
                    .set_flat_bottom()
                    .set_tissue_culture_treated()
                    .set_standard_lid()
                    .build_messages())

        assert len(messages) == 10
        assert "A PMR 8" in messages
        assert "A PMC 12" in messages
        assert "A PMM Corning" in messages
        assert "A PMm 3595" in messages
        assert "A PMB f" in messages
        assert "A PMT t" in messages
        assert "A PML l" in messages


class TestFullExperimentBuilder:
    """Test suite for FullExperimentBuilder"""

    def test_experiment_and_plate(self):
        """Test building both experiment and plate"""
        builder = FullExperimentBuilder()

        builder.plate().set_96_well_plate()
        builder.experiment().set_interval(60)

        commands = builder.build_all()

        # Should have plate commands first, then experiment commands
        assert len(commands) > 5

        # First commands should be plate template (A P M ...)
        messages = [cmd.raw_message for cmd in commands]
        plate_msgs = [m for m in messages if m.startswith("A P")]
        exp_msgs = [m for m in messages if m.startswith("A i")]

        assert len(plate_msgs) == 5  # 96-well preset has 5 commands
        assert len(exp_msgs) == 1

    def test_build_all_messages(self):
        """Test building all messages as strings"""
        builder = FullExperimentBuilder()

        builder.plate().set_96_well_plate()
        builder.experiment().set_interval(60)

        messages = builder.build_all_messages()

        assert len(messages) == 8  # 5 plate + 3 experiment
        assert all(isinstance(m, str) for m in messages)

    def test_switching_contexts(self):
        """Test switching between plate and experiment contexts"""
        builder = FullExperimentBuilder()

        # Configure plate
        plate_builder = builder.plate()
        plate_builder.set_96_well_plate()

        # Switch to experiment
        exp_builder = builder.experiment()
        exp_builder.set_interval(60)

        # Switch back to plate
        plate_builder2 = builder.plate()
        plate_builder2.set_manufacturer("Test")

        # Should be same builder
        assert plate_builder is plate_builder2

        commands = builder.build_all()
        assert len(commands) == 9  # 5 + 1 plate, 1 experiment

    def test_complete_workflow(self):
        """Test a complete experiment setup workflow"""
        builder = FullExperimentBuilder(command_id="test")

        # Configure plate template
        builder.plate().set_96_well_plate()
        builder.plate().set_manufacturer("Greiner")
        builder.plate().set_model("655101")
        builder.plate().set_flat_bottom()
        builder.plate().set_tissue_culture_treated()

        # Configure experiment
        builder.experiment().set_name("Growth Curve")
        builder.experiment().set_user("scientist@lab.com")
        builder.experiment().set_interval(300)
        builder.experiment().set_duration(86400)
        builder.experiment().set_type(ExperimentType.KINETIC)

        messages = builder.build_all_messages()

        # Check that all commands have the command ID
        assert all("*test " in m for m in messages)

        # Check we have both plate and experiment commands
        plate_count = sum(1 for m in messages if "A*test PM" in m)
        exp_count = sum(1 for m in messages if m.startswith(
            "A*test") and "P" not in m)

        assert plate_count == 9
        assert exp_count == 7   # reset + 5 settings + execute


class TestBuilderEdgeCases:
    """Test edge cases and error conditions"""

    def test_empty_builder(self):
        """Test building with no commands"""
        builder = ExperimentBuilder()
        commands = builder.build()

        assert len(commands) == 2
        assert commands[0].raw_message == 'A 0'
        assert commands[1].raw_message == 'A .'

    def test_multiple_builds(self):
        """Test that build() doesn't clear commands"""
        builder = ExperimentBuilder()
        builder.set_interval(60)

        commands1 = builder.build()
        commands2 = builder.build()

        assert len(commands1) == 3
        assert len(commands2) == 3
        assert commands1[1].raw_message == commands2[1].raw_message

    def test_build_returns_copy(self):
        """Test that build() returns a copy of commands"""
        builder = ExperimentBuilder()
        builder.set_interval(60)

        commands1 = builder.build()
        commands2 = builder.build()

        # Should be different list objects
        assert commands1 is not commands2

        # But contain equivalent commands
        assert len(commands1) == len(commands2)

    def test_infinite_duration(self):
        """Test setting infinite duration"""
        builder = ExperimentBuilder()
        commands = builder.set_duration(-1).build()

        assert commands[1].raw_message == "A d -1"

    def test_empty_name(self):
        """Test empty experiment name (auto-generate)"""
        builder = ExperimentBuilder()
        commands = builder.set_name("").build()

        # Should still create command but without name parameter
        assert commands[1].raw_message == "A n"

    def test_empty_experiment_id(self):
        """Test empty experiment ID (auto-generate)"""
        builder = ExperimentBuilder()
        commands = builder.set_experiment_id("").build()

        assert commands[1].raw_message == "A I"

    def test_detector_assignment_multiple_wells(self):
        """Test assigning multiple detectors to wells"""
        builder = PlateTemplateBuilder()

        # Assign 3 detectors to 3 wells
        for i in range(3):
            builder.assign_detector_to_well(i + 1, i)

        commands = builder.build()
        assert len(commands) == 3

        messages = [cmd.raw_message for cmd in commands]
        assert "A PMW 1 0" in messages
        assert "A PMW 2 1" in messages
        assert "A PMW 3 2" in messages


class TestBuilderIntegration:
    """Integration tests with realistic scenarios"""

    def test_kinetic_growth_curve(self):
        """Test setting up a typical bacterial growth curve"""
        builder = FullExperimentBuilder()
        (builder
            .experiment()
            .reset()
            .set_name("E. coli Growth Curve")
            .set_user("microbiologist@lab.edu")
            .set_interval(600)      # 10 minutes
            .set_duration(43200)    # 12 hours
            .set_type(ExperimentType.KINETIC)
         )

        (builder
         .plate()
         .set_96_well_plate()
         .set_manufacturer("Corning")
         .set_flat_bottom()
         .set_tissue_culture_treated())
        builder.experiment()

        messages = (
            builder.build_all_messages())

        assert len(messages) > 10
        assert any("Growth Curve" in m for m in messages)
        assert any("A t 1" in m for m in messages)  # Kinetic type

    def test_endpoint_elisa(self):
        """Test setting up an ELISA endpoint read"""
        builder = FullExperimentBuilder()

        builder.plate().set_96_well_plate().set_manufacturer("Thermo").set_flat_bottom()
        builder.experiment().reset().set_name("ELISA Plate 1").set_type(
            ExperimentType.ENDPOINT)
        messages = (builder.build_all_messages())

        assert any("ELISA" in m for m in messages)
        assert any("A t 0" in m for m in messages)  # Endpoint type

    def test_6_well_cell_culture(self):
        """Test 6-well plate for cell culture"""
        builder = FullExperimentBuilder()

        # Build plate with detector assignments
        plate = builder.plate()
        plate.set_6_well_plate()
        plate.set_tissue_culture_treated()

        # Assign detectors (example: 4 detectors per well for 6 wells)
        for well in range(6):
            for detector in range(4):
                plate.assign_detector_to_well(
                    detector_num=well * 4 + detector + 1,
                    well_num=well
                )

        messages = builder.build_all_messages()

        # Should have 6-well config + 24 detector assignments
        detector_msgs = [m for m in messages if "A PMW" in m]
        assert len(detector_msgs) == 24


class TestExperimentResults:
    def test_basic_case(self):

        # Create results container
        results = ExperimentResults(
            experiment_id="A3C14980-B645-4D8D-8913-DF4231D32B95",
            experiment_name="Growth Curve Test",
            wavelengths=[450],
            plate_layout={'rows': 8, 'cols': 12}
        )

        message = ':12345*abcd D A3C14980-B645-4D8D-8913-DF4231D32B95\t1654625593\t28.5313\t262153\t0.0175\t0.0149\t0.0203\t0.0097\t0.0219\t0.0110\t0.0082\t0.0110\t0.0168\t0.0097\t0.0160\t0.0100\t0.0172\t0.0084\t0.0234\t0.0119\t0.0163\t0.0125\t0.0166\t0.0144\t0.0199\t0.0132\t0.0194\t0.0096\t0.0165\t0.0098\t0.0177\t0.0095\t0.0155\t0.0126\t0.0167\t0.0124\t0.0168\t0.0120\t0.0172\t0.0085\t0.0215\t0.0118\t0.0157\t0.0109\t0.0186\t0.0093\t0.0204\t0.0095\t0.0176\t0.0094\t0.0190\t0.0103\t0.0168\t0.0096\t0.0203\t0.0136\t0.0197\t0.0108\t0.0173\t0.0094\t0.0158\t0.0084\t0.0161\t0.0095\t0.0180\t0.0099\t0.0170\t0.0125\t0.0183\t0.0110\t0.0228\t0.0099\t0.0203\t0.0129\t0.0159\t0.0102\t0.0193\t0.0096\t0.0214\t0.0099\t0.0167\t0.0123\t0.0181\t0.0117\t0.0019\t0.0089\t0.0018\t0.0100\t0.0164\t0.0108\t0.0208\t0.0094\t0.0158\t0.0153\t0.0262\t0.0079\t0.0169\t0.0095\t0.0199\t0.0081'
        DataTransmissionParser.parse_data_message(message, results)

        # Simulate adding data points
        print(results)
        print(f"\nWells: {results.get_wells()}")
        print(f"\nTimeseries for A1:")
        for ts, val in results.get_well_timeseries('A1'):
            print(f'  {ts}: {float(val):.3f}')

        # Export examples
        print("\n=== JSON Export ===")
        print(results.to_json())

        print("\n=== CSV Export (long) ===")
        print(results.to_csv(format="long")[:300] + "...")

        print("\n=== CSV Export (wide) ===")
        print(results.to_csv(format="wide")[:300] + "...")

    def test_real_case(self):
        message = ':2000232 D"39034871-4198-4686-A58B-936B265F3B05"   1764083902      22.9277 0               -0.0289 -0.0366 -0.0165 0.0363  -0.0266 -0.0461 -0.0282 -0.0039 -0.0703 -0.0297 0.0667  0.0221    -0.0692 -0.0273 -0.0476 0.0192  -0.0123 -0.0816 -0.0372 -0.0243 0.0100  -0.0152 0.0232  0.0453  -0.0405 -0.0159 0.0414  -0.0120 -0.0088 0.0522  -0.0039 -0.0088 -0.0226 -0.0004 -0.0773   0.0397  -0.0636 -0.0468 -0.0638 -0.0613 0.0063  -0.0175 -0.0195 0.0063  -0.0360 -0.0387 -0.0392 -0.0519 -0.0707 -0.0637 -0.0608 -27.9529        -INF    -4.3024 -INF    -4.3980 -INF      -INF    -28.2724        19.2329 -INF    -INF    -INF    5.2151  -INF    -INF    -INF    20.3716 NaN     -27.6429        -INF    -INF    NaN     -4.1936 20.4781 -5.5370 -INF    -INF      1.1772  NaN     NaN     13.7477 -5.2987 -INF    -INF    -4.8702 -5.4781 NaN     NaN     NaN     -INF    -INF    NaN     -9.7185 13.8419 14.9864'
        # Create results container
        results = ExperimentResults(
            experiment_id="39034871-4198-4686-A58B-936B265F3B05",
            experiment_name="Growth Curve Test",
            wavelengths=[450],
            plate_layout={'rows': 8, 'cols': 12}
        )
        DataTransmissionParser.parse_data_message(message, results)

        # Simulate adding data points
        print(results)


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
