"""Tests for the Workspace service. Covers workspace creation/update, with a focus on the `planning_mode` field (explicit solo/couple planning intent — not inferred from member count), plus renaming/removing a placeholder partner (`rename_member` / `remove_member`). """ from datetime import date, datetime from uuid import uuid4 import pytest from app.entities.leave_wish import LeaveWish from app.entities.parental_leave import ParentalLeaveFetch, TierData from app.entities.planned_leave import PlannedLeaveDay from app.entities.user import User from app.entities.vacation_days import VacationDays from app.repositories_impl.sqlite.leave_wish_repository import SQLiteLeaveWishRepository from app.repositories_impl.sqlite.parental_leave_repository import SQLiteParentalLeaveRepository from app.repositories_impl.sqlite.planned_leave_repository import SQLitePlannedLeaveRepository from app.repositories_impl.sqlite.user_repository import SQLiteUserRepository from app.repositories_impl.sqlite.vacation_days_repository import SQLiteVacationDaysRepository from app.services.workspace_service import WorkspaceService from app.utils.errors import NotFoundError, ValidationError from tests.conftest import SeededIds class TestWorkspacePlanningMode: """Test planning_mode defaults and validation.""" async def test_create_workspace_defaults_to_solo( self, workspace_service: WorkspaceService, ) -> None: """A newly created workspace defaults to solo planning mode.""" ws = await workspace_service.create_workspace("Test WS") assert ws.planning_mode == "solo" async def test_update_planning_mode_to_couple( self, workspace_service: WorkspaceService, ) -> None: """Updating planning_mode to a valid value ('couple') succeeds.""" ws = await workspace_service.create_workspace("Test WS") updated = await workspace_service.update_workspace(ws.id, planning_mode="couple") assert updated.planning_mode == "couple" async def test_update_planning_mode_back_to_solo( self, workspace_service: WorkspaceService, ) -> None: """Updating planning_mode back to 'solo' succeeds.""" ws = await workspace_service.create_workspace("Test WS") await workspace_service.update_workspace(ws.id, planning_mode="couple") updated = await workspace_service.update_workspace(ws.id, planning_mode="solo") assert updated.planning_mode == "solo" async def test_update_planning_mode_invalid_raises( self, workspace_service: WorkspaceService, ) -> None: """An invalid planning_mode value raises ValidationError.""" ws = await workspace_service.create_workspace("Test WS") with pytest.raises(ValidationError, match="planning_mode must be one of"): await workspace_service.update_workspace(ws.id, planning_mode="throuple") async def test_update_planning_mode_missing_workspace_raises( self, workspace_service: WorkspaceService, ) -> None: """Updating planning_mode on a missing workspace raises NotFoundError.""" with pytest.raises(NotFoundError): await workspace_service.update_workspace(uuid4(), planning_mode="couple") async def test_update_name_and_planning_mode_together( self, workspace_service: WorkspaceService, ) -> None: """Multiple fields can be updated in the same call.""" ws = await workspace_service.create_workspace("Old Name") updated = await workspace_service.update_workspace( ws.id, name="New Name", planning_mode="couple" ) assert updated.name == "New Name" assert updated.planning_mode == "couple" class TestRenameMember: """Rename a workspace member's display name (placeholder or self).""" async def test_rename_placeholder_happy_path( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") renamed = await workspace_service.rename_member( seeded_ids.workspace_id, placeholder.uuid, "Samuel" ) assert renamed.name == "Samuel" assert renamed.uuid == placeholder.uuid async def test_rename_own_profile_is_allowed( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: """No other endpoint can fix a typo'd own name, so this route allows it.""" renamed = await workspace_service.rename_member( seeded_ids.workspace_id, seeded_ids.personal_data_uuid, "Corrected Name" ) assert renamed.name == "Corrected Name" async def test_rename_non_member_raises_not_found( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: with pytest.raises(NotFoundError): await workspace_service.rename_member(seeded_ids.workspace_id, uuid4(), "Nope") async def test_rename_member_of_different_workspace_raises_not_found( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: """A real personal_data_uuid from a DIFFERENT workspace must 404, not succeed.""" other_ws = await workspace_service.create_workspace("Other WS") other_placeholder = await workspace_service.add_placeholder_parent(other_ws.id, "Other") with pytest.raises(NotFoundError): await workspace_service.rename_member( seeded_ids.workspace_id, other_placeholder.uuid, "Hijacked" ) async def test_rename_missing_workspace_raises_not_found( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: with pytest.raises(NotFoundError): await workspace_service.rename_member(uuid4(), seeded_ids.personal_data_uuid, "Whoever") async def test_rename_blank_name_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") with pytest.raises(ValidationError, match="empty or whitespace-only"): await workspace_service.rename_member(seeded_ids.workspace_id, placeholder.uuid, " ") async def test_add_placeholder_blank_name_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: """add_placeholder_parent applies the same blank-name rule as rename_member.""" with pytest.raises(ValidationError, match="empty or whitespace-only"): await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, " ") class TestRemoveMember: """Remove (soft-delete) a placeholder partner, guarding against orphaned data.""" async def test_remove_placeholder_happy_path( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid ) members = await workspace_service.list_members(seeded_ids.workspace_id) assert placeholder.uuid not in {m.uuid for m in members} async def test_remove_non_member_raises_not_found( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: with pytest.raises(NotFoundError): await workspace_service.remove_member( seeded_ids.workspace_id, uuid4(), seeded_ids.personal_data_uuid ) async def test_remove_self_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, ) -> None: with pytest.raises(ValidationError, match="own profile"): await workspace_service.remove_member( seeded_ids.workspace_id, seeded_ids.personal_data_uuid, seeded_ids.personal_data_uuid, ) async def test_remove_member_with_login_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, user_repo: SQLiteUserRepository, ) -> None: """A member with their own login is not actually a placeholder.""" placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await user_repo.create( User( id=uuid4(), personal_data_uuid=placeholder.uuid, email="sam@example.com", hashed_password="hash", created_at=datetime.now(), ) ) with pytest.raises(ValidationError, match="own login"): await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid ) async def test_remove_member_with_leave_wish_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, leave_wish_repo: SQLiteLeaveWishRepository, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await leave_wish_repo.create( LeaveWish( id=uuid4(), workspace_id=seeded_ids.workspace_id, child_id=None, name="Sommar", window_start_month=6, window_start_day=1, window_end_month=8, window_end_day=31, target_weeks=4, recurrence="yearly", first_year=2026, last_year=None, priority=1, parent_mode="specific", parent_personal_data_uuid=placeholder.uuid, ) ) with pytest.raises(ValidationError, match="leave_wishes"): await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid ) async def test_remove_member_with_planned_leave_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, planned_leave_repo: SQLitePlannedLeaveRepository, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await planned_leave_repo.create( PlannedLeaveDay( id=uuid4(), workspace_id=seeded_ids.workspace_id, child_id=seeded_ids.child_id, parent_personal_data_uuid=placeholder.uuid, date=date(2026, 6, 1), day_type="sjukpenning", ) ) with pytest.raises(ValidationError, match="planned_leave"): await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid ) async def test_remove_member_with_parental_leave_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, parental_leave_repo: SQLiteParentalLeaveRepository, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await parental_leave_repo.create( ParentalLeaveFetch( id=uuid4(), personal_data_uuid=placeholder.uuid, workspace_id=seeded_ids.workspace_id, child_id=seeded_ids.child_id, source="manual", fetched_at=datetime.now(), total_days=480, days_used=0, days_left=480, reserved_days_total=90, reserved_days_left=90, transferable_days_left=390, dubbeldagar_left=60, tier_data=TierData( sjukpenning_total=390, sjukpenning_used=0, sjukpenning_left=390, lagsta_niva_total=90, lagsta_niva_used=0, lagsta_niva_left=90, ), ) ) with pytest.raises(ValidationError, match="parental_leave"): await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid ) async def test_remove_member_with_vacation_days_raises_validation_error( self, workspace_service: WorkspaceService, seeded_ids: SeededIds, vacation_repo: SQLiteVacationDaysRepository, ) -> None: placeholder = await workspace_service.add_placeholder_parent(seeded_ids.workspace_id, "Sam") await vacation_repo.create( VacationDays( id=uuid4(), personal_data_uuid=placeholder.uuid, workspace_id=seeded_ids.workspace_id, year=2026, total_days=25, ) ) with pytest.raises(ValidationError, match="vacation_days"): await workspace_service.remove_member( seeded_ids.workspace_id, placeholder.uuid, seeded_ids.personal_data_uuid )