import os
from unittest.mock import Mock
from uuid import UUID, uuid4

import pytest
from horizon_data_core.base_types import (
    BeamgramSpecification,
    BearingTimeRecordSpecification,
    DataStream,
    Mission,
    MissionPlatform,
    Ontology,
    OntologyClass,
    Platform,
    PlatformBeamgramSpecification,
    PlatformBearingTimeRecordSpecification,
)
from horizon_data_core.client import PostgresClient
from horizon_data_core.sdk import HorizonSDK
from sqlalchemy import text


@pytest.fixture
def initialized_sdk() -> HorizonSDK:
    """Fixture to initialize SDK with database connection."""
    postgres_client = PostgresClient(
        user=os.environ.get("OWNER_PGUSER", "postgres"),
        password=os.environ.get("OWNER_PGPASSWORD", "password"),
        host=os.environ.get("PGHOST", "localhost"),
        port=int(os.environ.get("PGPORT", 5432)),
        database=os.environ.get("PGDATABASE", "horizon"),
        sslmode="disable",
        channel_binding="prefer",
    )
    iceberg_catalog_properties: dict[str, str | None] = {
        "uri": "http://localhost:8181",
        "warehouse": "file://./iceberg/warehouse",
    }
    return HorizonSDK(postgres_client, iceberg_catalog_properties, None)


@pytest.fixture
def platform_kind_id(initialized_sdk: HorizonSDK) -> UUID:
    """Fixture to create a platform kind for use in repository tests."""
    kind_id = uuid4()
    with initialized_sdk.postgres_client.session() as session:
        session.execute(
            text("INSERT INTO horizon_public.platform_kind (id, name) VALUES (:id, :name)"),
            {"id": kind_id, "name": "Test Platform Kind"}
        )
        session.commit()
    return kind_id


def test_create_sdk() -> None:
    """Test that the SDK can be created."""
    postgres_client = PostgresClient(
        user="postgres",
        password="password",
        host="localhost",
        port=5432,
        database="horizon",
        sslmode="disable",
        channel_binding="prefer",
    )
    iceberg_catalog_properties: dict[str, str | None] = {
        "uri": "http://localhost:8181",
        "warehouse": "file://./iceberg/warehouse",
    }
    sdk = HorizonSDK(postgres_client, iceberg_catalog_properties, None)
    assert sdk is not None


# Repository Operation Tests

def test_repository_insert(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository insert() creates a new record."""
    platform = Platform(
        id=uuid4(),
        kind_id=platform_kind_id,
        name="Test Platform",
    )
    result = initialized_sdk.platforms.insert(platform)
    assert result.id == platform.id
    assert result.name == "Test Platform"
    assert result.kind_id == platform_kind_id


def test_repository_insert_batch(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository insert_batch() creates multiple records."""
    platforms = [
        Platform(id=uuid4(), kind_id=platform_kind_id, name="Platform 1"),
        Platform(id=uuid4(), kind_id=platform_kind_id, name="Platform 2"),
    ]
    result = initialized_sdk.platforms.insert_batch(platforms)
    assert len(result) == 2
    assert all(r.name in ["Platform 1", "Platform 2"] for r in result)


def test_repository_insert_batch_empty(initialized_sdk: HorizonSDK) -> None:
    """Test that repository insert_batch() returns empty list for empty input."""
    result = initialized_sdk.platforms.insert_batch([])
    assert result == []


def test_repository_read(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository read() retrieves a record by ID."""
    platform = Platform(id=uuid4(), kind_id=platform_kind_id, name="Test Platform")
    created = initialized_sdk.platforms.insert(platform)
    assert created.id is not None

    result = initialized_sdk.platforms.read(created.id)
    assert result is not None
    assert result.id == created.id
    assert result.name == "Test Platform"


def test_repository_read_not_found(initialized_sdk: HorizonSDK) -> None:
    """Test that repository read() returns None for non-existent ID."""
    result = initialized_sdk.platforms.read(uuid4())
    assert result is None


def test_repository_update(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository update() modifies an existing record."""
    platform = Platform(id=uuid4(), kind_id=platform_kind_id, name="Original Name")
    created = initialized_sdk.platforms.insert(platform)
    assert created.id is not None

    updated_platform = Platform(id=created.id, kind_id=platform_kind_id, name="Updated Name")
    result = initialized_sdk.platforms.update(updated_platform)
    assert result.name == "Updated Name"

    # Verify update persisted
    read_result = initialized_sdk.platforms.read(created.id)
    assert read_result is not None
    assert read_result.name == "Updated Name"


def test_repository_delete(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository delete() removes a record."""
    platform = Platform(id=uuid4(), kind_id=platform_kind_id, name="Platform to delete")
    created = initialized_sdk.platforms.insert(platform)
    assert created.id is not None

    deleted = initialized_sdk.platforms.delete(created.id)
    assert deleted is True

    # Verify it's gone
    result = initialized_sdk.platforms.read(created.id)
    assert result is None


def test_repository_delete_not_found(initialized_sdk: HorizonSDK) -> None:
    """Test that repository delete() returns False for non-existent ID."""
    result = initialized_sdk.platforms.delete(uuid4())
    assert result is False


def test_repository_list(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository list() returns all records."""
    platform1 = initialized_sdk.platforms.insert(Platform(id=uuid4(), kind_id=platform_kind_id, name="Platform 1"))
    platform2 = initialized_sdk.platforms.insert(Platform(id=uuid4(), kind_id=platform_kind_id, name="platform 2"))

    result = initialized_sdk.platforms.list()
    assert len(result) >= 2
    platform_ids = {e.id for e in result}
    assert platform1.id in platform_ids
    assert platform2.id in platform_ids


def test_repository_list_with_filters(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository list() filters records correctly."""
    platform1 = initialized_sdk.platforms.insert(Platform(id=uuid4(), kind_id=platform_kind_id, name="Filtered Platform"))
    initialized_sdk.platforms.insert(Platform(id=uuid4(), kind_id=platform_kind_id, name="Other Platform"))

    # Filter by name
    result = initialized_sdk.platforms.list(name="Filtered Platform")  # type: ignore[arg-type]
    assert len(result) >= 1
    assert all(e.name == "Filtered Platform" for e in result)
    assert platform1.id in {e.id for e in result}

    # Filter by kind_id
    kind_result = initialized_sdk.platforms.list(kind_id=platform_kind_id)  # type: ignore[arg-type]
    assert len(kind_result) >= 2
    assert all(e.kind_id == platform_kind_id for e in kind_result)


def test_repository_upsert_create(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository upsert() creates a new record when it doesn't exist."""
    platform = Platform(id=uuid4(), kind_id=platform_kind_id, name="New Platform")
    result = initialized_sdk.platforms.upsert(platform, unique_fields=["id"], merge_fields=["name", "kind_id"])
    assert result.id == platform.id
    assert result.id is not None

    # Verify it was created
    read_result = initialized_sdk.platforms.read(result.id)
    assert read_result is not None
    assert read_result.name == "New Platform"


def test_repository_upsert_update(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository upsert() updates an existing record."""
    platform = Platform(id=uuid4(), kind_id=platform_kind_id, name="Original Name")
    created = initialized_sdk.platforms.insert(platform)
    assert created.id is not None

    # Upsert with updated name
    updated = Platform(id=created.id, kind_id=platform_kind_id, name="Upserted Name")
    result = initialized_sdk.platforms.upsert(updated, unique_fields=["id"], merge_fields=["name"])
    assert result.name == "Upserted Name"

    # Verify update persisted
    read_result = initialized_sdk.platforms.read(created.id)
    assert read_result is not None
    assert read_result.name == "Upserted Name"


def test_repository_upsert_batch(initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
    """Test that repository upsert_batch() creates or updates multiple records."""
    # Create one platform first
    existing = initialized_sdk.platforms.insert(Platform(id=uuid4(), kind_id=platform_kind_id, name="Existing"))
    assert existing.id is not None

    # Upsert batch with one existing and one new
    platforms = [
        Platform(id=existing.id, kind_id=platform_kind_id, name="Updated Existing"),
        Platform(id=uuid4(), kind_id=platform_kind_id, name="New Platform"),
    ]
    result = initialized_sdk.platforms.upsert_batch(platforms, unique_fields=["id"], merge_fields=["name", "kind_id"])
    assert len(result) == 2

    # Verify existing was updated
    read_existing = initialized_sdk.platforms.read(existing.id)
    assert read_existing is not None
    assert read_existing.name == "Updated Existing"

    # Verify new was created
    new_ids = {e.id for e in result}
    assert platforms[1].id in new_ids

def test_sdk_read_data_stream(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK read_data_stream calls the repository read method."""
    initialized_sdk.data_streams.read = Mock()  # type: ignore[method-assign]
    initialized_sdk.read_data_stream(uuid4())
    initialized_sdk.data_streams.read.assert_called_once()

def test_sdk_update_data_stream(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK update_data_stream calls the repository upsert method."""
    initialized_sdk.data_streams.upsert = Mock()  # type: ignore[method-assign]
    initialized_sdk.update_data_stream(Mock())
    initialized_sdk.data_streams.upsert.assert_called_once()

def test_sdk_delete_data_stream(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK delete_data_stream calls the repository delete method."""
    initialized_sdk.data_streams.delete = Mock()  # type: ignore[method-assign]
    initialized_sdk.delete_data_stream(uuid4())
    initialized_sdk.data_streams.delete.assert_called_once()

def test_sdk_list_data_streams(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK list_data_streams calls the repository list method."""
    initialized_sdk.data_streams.list = Mock()  # type: ignore[method-assign]
    initialized_sdk.list_data_streams()
    initialized_sdk.data_streams.list.assert_called_once()

def test_sdk_create_mission(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK create_mission calls the repository insert method."""
    initialized_sdk.missions.insert = Mock()  # type: ignore[method-assign]
    initialized_sdk.create_mission(Mock())
    initialized_sdk.missions.insert.assert_called_once()

def test_sdk_read_mission(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK read_mission calls the repository read method."""
    initialized_sdk.missions.read = Mock()  # type: ignore[method-assign]
    initialized_sdk.read_mission(Mock())
    initialized_sdk.missions.read.assert_called_once()

def test_sdk_update_mission(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK update_mission calls the repository update method."""
    initialized_sdk.missions.update = Mock()  # type: ignore[method-assign]
    initialized_sdk.update_mission(Mock())
    initialized_sdk.missions.update.assert_called_once()

def test_sdk_delete_mission(initialized_sdk: HorizonSDK) -> None:
    """Test that SDK read_mission calls the repository delete method."""
    initialized_sdk.missions.delete = Mock()  # type: ignore[method-assign]
    initialized_sdk.delete_mission(Mock())
    initialized_sdk.missions.delete.assert_called_once()


# Unit tests: models can be constructed without an id


class TestModelConstructionWithoutId:
    """Test that all BasePostgresModel subclasses can be constructed without providing an id."""

    def test_mission_without_id(self) -> None:
        mission = Mission(name="Test Mission")
        assert mission.id is None

    def test_platform_without_id(self) -> None:
        platform = Platform(kind_id=uuid4(), name="Test Platform")
        assert platform.id is None

    def test_data_stream_without_id(self) -> None:
        ds = DataStream(platform_id=uuid4())
        assert ds.id is None

    def test_ontology_without_id(self) -> None:
        ontology = Ontology(name="Test Ontology")
        assert ontology.id is None

    def test_ontology_class_without_id(self) -> None:
        oc = OntologyClass(ontology_id=uuid4(), name="Test Class")
        assert oc.id is None

    def test_mission_platform_without_id(self) -> None:
        mp = MissionPlatform(mission_id=uuid4(), platform_id=uuid4())
        assert mp.id is None

    def test_beamgram_specification_without_id(self) -> None:
        spec = BeamgramSpecification(min_frequency=100.0, max_frequency=200.0)
        assert spec.id is None

    def test_bearing_time_record_specification_without_id(self) -> None:
        spec = BearingTimeRecordSpecification(min_frequency=100.0, max_frequency=200.0)
        assert spec.id is None

    def test_platform_beamgram_specification_without_id(self) -> None:
        pbs = PlatformBeamgramSpecification(platform_id=uuid4(), beamgram_specification_id=uuid4())
        assert pbs.id is None

    def test_platform_bearing_time_record_specification_without_id(self) -> None:
        pbtr = PlatformBearingTimeRecordSpecification(
            platform_id=uuid4(), bearing_time_record_specification_id=uuid4()
        )
        assert pbtr.id is None


# Integration tests: insert without id, server assigns via gen_random_uuid()


class TestServerIdAssignment:
    """Test that Postgres assigns an id via gen_random_uuid() when no id is provided."""

    def test_insert_mission_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """Mission has no FK dependencies."""
        mission = Mission(name="Auto ID Mission")
        result = initialized_sdk.missions.insert(mission)
        assert result.id is not None
        assert result.name == "Auto ID Mission"

        # Verify it persisted and is readable
        read_back = initialized_sdk.missions.read(result.id)
        assert read_back is not None
        assert read_back.id == result.id

    def test_insert_ontology_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """Ontology has no FK dependencies."""
        ontology = Ontology(name="Auto ID Ontology")
        result = initialized_sdk.ontologies.insert(ontology)
        assert result.id is not None
        assert result.name == "Auto ID Ontology"

    def test_insert_beamgram_specification_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """BeamgramSpecification has no FK dependencies."""
        spec = BeamgramSpecification(min_frequency=100.0, max_frequency=500.0, nfft=1024)
        result = initialized_sdk.beamgram_specifications.insert(spec)
        assert result.id is not None
        assert result.min_frequency == 100.0

    def test_insert_bearing_time_record_specification_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """BearingTimeRecordSpecification has no FK dependencies."""
        spec = BearingTimeRecordSpecification(min_frequency=50.0, max_frequency=400.0, nfft=512)
        result = initialized_sdk.bearing_time_record_specifications.insert(spec)
        assert result.id is not None
        assert result.nfft == 512

    def test_insert_platform_without_id(self, initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
        """Platform requires a platform_kind FK."""
        platform = Platform(kind_id=platform_kind_id, name="Auto ID Platform")
        result = initialized_sdk.platforms.insert(platform)
        assert result.id is not None
        assert result.name == "Auto ID Platform"
        assert result.kind_id == platform_kind_id

    def test_insert_data_stream_without_id(self, initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
        """DataStream requires a platform FK."""
        platform = initialized_sdk.platforms.insert(Platform(kind_id=platform_kind_id, name="DS Parent"))
        assert platform.id is not None

        ds = DataStream(platform_id=platform.id, name="Auto ID Stream")
        result = initialized_sdk.data_streams.insert(ds)
        assert result.id is not None
        assert result.platform_id == platform.id

    def test_insert_ontology_class_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """OntologyClass requires an ontology FK."""
        ontology = initialized_sdk.ontologies.insert(Ontology(name="Parent Ontology"))
        assert ontology.id is not None

        oc = OntologyClass(ontology_id=ontology.id, name="Auto ID Class")
        result = initialized_sdk.ontology_classes.insert(oc)
        assert result.id is not None
        assert result.ontology_id == ontology.id

    def test_insert_mission_platform_without_id(self, initialized_sdk: HorizonSDK, platform_kind_id: UUID) -> None:
        """MissionPlatform requires mission + platform FKs."""
        mission = initialized_sdk.missions.insert(Mission(name="MP Mission"))
        platform = initialized_sdk.platforms.insert(Platform(kind_id=platform_kind_id, name="MP Platform"))
        assert mission.id is not None
        assert platform.id is not None

        mp = MissionPlatform(mission_id=mission.id, platform_id=platform.id)
        result = initialized_sdk.mission_platforms.insert(mp)
        assert result.id is not None
        assert result.mission_id == mission.id
        assert result.platform_id == platform.id

    def test_insert_platform_beamgram_specification_without_id(
        self, initialized_sdk: HorizonSDK, platform_kind_id: UUID
    ) -> None:
        """PlatformBeamgramSpecification requires platform + beamgram_specification FKs."""
        platform = initialized_sdk.platforms.insert(Platform(kind_id=platform_kind_id, name="PBS Platform"))
        spec = initialized_sdk.beamgram_specifications.insert(BeamgramSpecification(min_frequency=100.0))
        assert platform.id is not None
        assert spec.id is not None

        pbs = PlatformBeamgramSpecification(platform_id=platform.id, beamgram_specification_id=spec.id)
        result = initialized_sdk.platform_beamgram_specifications.insert(pbs)
        assert result.id is not None
        assert result.platform_id == platform.id
        assert result.beamgram_specification_id == spec.id

    def test_insert_platform_bearing_time_record_specification_without_id(
        self, initialized_sdk: HorizonSDK, platform_kind_id: UUID
    ) -> None:
        """PlatformBearingTimeRecordSpecification requires platform + btr_specification FKs."""
        platform = initialized_sdk.platforms.insert(Platform(kind_id=platform_kind_id, name="PBTR Platform"))
        spec = initialized_sdk.bearing_time_record_specifications.insert(
            BearingTimeRecordSpecification(min_frequency=50.0)
        )
        assert platform.id is not None
        assert spec.id is not None

        pbtr = PlatformBearingTimeRecordSpecification(
            platform_id=platform.id, bearing_time_record_specification_id=spec.id
        )
        result = initialized_sdk.platform_bearing_time_record_specifications.insert(pbtr)
        assert result.id is not None
        assert result.platform_id == platform.id
        assert result.bearing_time_record_specification_id == spec.id

    def test_upsert_without_id_creates_new(self, initialized_sdk: HorizonSDK) -> None:
        """Upsert with no id should create a new record with a server-assigned id."""
        mission = Mission(name="Upsert No ID Mission")
        result = initialized_sdk.missions.upsert(mission)
        assert result.id is not None
        assert result.name == "Upsert No ID Mission"

    def test_insert_batch_without_id(self, initialized_sdk: HorizonSDK) -> None:
        """Batch insert with no ids should assign unique server-generated ids."""
        missions = [Mission(name="Batch 1"), Mission(name="Batch 2"), Mission(name="Batch 3")]
        results = initialized_sdk.missions.insert_batch(missions)
        assert len(results) == 3
        ids = {r.id for r in results}
        assert None not in ids
        assert len(ids) == 3  # All unique
