Feature: LockService coverage – uncovered branches and edge cases
  As a developer
  I want full coverage of every branch in lock_service.py
  So that no edge case is left untested

  Background:
    Given I have a fresh lock service with in-memory database

  # ------------------------------------------------------------------
  # Validation: release method
  # ------------------------------------------------------------------

  Scenario: Release rejects empty resource_type
    When I try to release with an empty resource_type
    Then a lock_service validation error should be raised with message containing "resource_type must not be empty"

  Scenario: Release rejects invalid resource_type
    When I try to release with resource_type "widget" owner "o" resource_id "r1"
    Then a lock_service validation error should be raised with message containing "resource_type must be one of"

  Scenario: Release rejects empty resource_id
    When I try to release with an empty resource_id
    Then a lock_service validation error should be raised with message containing "resource_id must not be empty"

  # ------------------------------------------------------------------
  # Validation: renew method
  # ------------------------------------------------------------------

  Scenario: Renew rejects empty resource_type
    When I try to renew with an empty resource_type
    Then a lock_service validation error should be raised with message containing "resource_type must not be empty"

  Scenario: Renew rejects invalid resource_type
    When I try to renew with resource_type "widget" owner "o" resource_id "r1"
    Then a lock_service validation error should be raised with message containing "resource_type must be one of"

  Scenario: Renew rejects empty resource_id
    When I try to renew with an empty resource_id
    Then a lock_service validation error should be raised with message containing "resource_id must not be empty"

  Scenario: Renew rejects TTL below minimum
    When I try to renew with TTL 2 owner "o" resource_type "plan" resource_id "r1"
    Then a lock_service validation error should be raised with message containing "ttl_seconds must be >="

  Scenario: Renew rejects TTL above maximum
    When I try to renew with TTL 7200 owner "o" resource_type "plan" resource_id "r1"
    Then a lock_service validation error should be raised with message containing "ttl_seconds must be <="

  # ------------------------------------------------------------------
  # Validation: is_locked method
  # ------------------------------------------------------------------

  Scenario: is_locked rejects empty resource_type
    When I try to check is_locked with an empty resource_type
    Then a lock_service validation error should be raised with message containing "resource_type must not be empty"

  Scenario: is_locked rejects invalid resource_type
    When I try to check is_locked with resource_type "widget" resource_id "r1"
    Then a lock_service validation error should be raised with message containing "resource_type must be one of"

  Scenario: is_locked rejects empty resource_id
    When I try to check is_locked with an empty resource_id
    Then a lock_service validation error should be raised with message containing "resource_id must not be empty"

  # ------------------------------------------------------------------
  # TTL boundary values on acquire
  # ------------------------------------------------------------------

  Scenario: Acquire with explicit minimum TTL succeeds
    When I acquire a lock with TTL 5 owner "owner-min" resource_type "plan" resource_id "p-min"
    Then the lock_service result should be true

  Scenario: Acquire with explicit maximum TTL succeeds
    When I acquire a lock with TTL 3600 owner "owner-max" resource_type "plan" resource_id "p-max"
    Then the lock_service result should be true

  Scenario: Acquire with custom valid TTL succeeds
    When I acquire a lock with TTL 120 owner "owner-cust" resource_type "project" resource_id "proj-cust"
    Then the lock_service result should be true

  # ------------------------------------------------------------------
  # Re-entrant acquisition on project resource
  # ------------------------------------------------------------------

  Scenario: Same owner re-acquires a project lock
    Given owner "owner-a" already holds a project lock on "proj-re"
    When I acquire a lock with TTL 300 owner "owner-a" resource_type "project" resource_id "proj-re"
    Then the lock_service result should be true

  # ------------------------------------------------------------------
  # Release by wrong owner returns False
  # ------------------------------------------------------------------

  Scenario: Release returns false when owner does not match
    Given owner "owner-a" already holds a plan lock on "plan-wrong"
    When I try to release with resource_type "plan" owner "wrong-owner" resource_id "plan-wrong"
    Then the lock_service result should be false

  # ------------------------------------------------------------------
  # is_locked on expired lock returns False
  # ------------------------------------------------------------------

  Scenario: is_locked returns false for an expired lock
    Given an expired lock exists for owner "old" on plan "plan-exp"
    When I check is_locked for resource_type "plan" resource_id "plan-exp"
    Then the lock_service result should be false

  # ------------------------------------------------------------------
  # Zero-count paths (no log emitted)
  # ------------------------------------------------------------------

  Scenario: release_all_for_owner returns zero when owner has no locks
    When I release all locks for owner_id "ghost"
    Then the lock_service integer result should be 0

  Scenario: cleanup_expired returns zero when no expired locks exist
    When I run cleanup of expired locks
    Then the lock_service integer result should be 0

  Scenario: count_stale_locks returns zero when no stale locks exist
    When I count stale locks via the service
    Then the lock_service integer result should be 0

  # ------------------------------------------------------------------
  # Exception rollback paths (mocked session)
  # ------------------------------------------------------------------

  Scenario: Acquire rolls back on unexpected exception
    Given a lock service whose session raises an unexpected error on execute
    When I try to acquire and expect a RuntimeError
    Then a RuntimeError should have been raised
    And the session should have been rolled back

  Scenario: Release rolls back on unexpected exception
    Given a lock service whose session raises an unexpected error on execute
    When I try to release and expect a RuntimeError
    Then a RuntimeError should have been raised
    And the session should have been rolled back

  Scenario: Renew rolls back on unexpected exception
    Given a lock service whose session raises an unexpected error on execute
    When I try to renew and expect a RuntimeError
    Then a RuntimeError should have been raised
    And the session should have been rolled back

  Scenario: release_all_for_owner rolls back on unexpected exception
    Given a lock service whose session raises an unexpected error on execute
    When I try to release_all_for_owner and expect a RuntimeError
    Then a RuntimeError should have been raised
    And the session should have been rolled back

  Scenario: cleanup_expired rolls back on unexpected exception
    Given a lock service whose session raises an unexpected error on execute
    When I try to cleanup_expired and expect a RuntimeError
    Then a RuntimeError should have been raised
    And the session should have been rolled back

  # ------------------------------------------------------------------
  # Acquire conflict rollback path
  # ------------------------------------------------------------------

  Scenario: Acquire rolls back the session on LockConflictError
    Given a lock service with a real database and owner "holder" holds plan "plan-conf"
    When I try to acquire plan "plan-conf" as "intruder" and capture the conflict error
    Then a LockConflictError should have been raised
    And the conflict owner should be "holder"

  # ------------------------------------------------------------------
  # Renew LockExpiredError rollback path
  # ------------------------------------------------------------------

  Scenario: Renew rolls back the session on LockExpiredError
    Given a lock service with a real database and an expired lock for "exp-owner" on plan "plan-exp-r"
    When I try to renew plan "plan-exp-r" as "exp-owner" and capture the expired error
    Then a LockExpiredError should have been raised
