# tests/unit/services/test_vendor_service.py
"""Unit tests for VendorService following the application's exception patterns."""
import uuid

import pytest

from app.exceptions import (
    InvalidVendorDataException,
    MarketplaceProductNotFoundException,
    ProductAlreadyExistsException,
    UnauthorizedVendorAccessException,
    ValidationException,
    VendorAlreadyExistsException,
    VendorNotFoundException,
)
from app.services.vendor_service import VendorService
from models.database.company import Company
from models.database.vendor import Vendor
from models.schema.product import ProductCreate
from models.schema.vendor import VendorCreate


@pytest.fixture
def admin_company(db, test_admin):
    """Create a test company for admin."""
    unique_id = str(uuid.uuid4())[:8]
    company = Company(
        name=f"Admin Company {unique_id}",
        owner_user_id=test_admin.id,
        contact_email=f"admin{unique_id}@company.com",
        is_active=True,
        is_verified=True,
    )
    db.add(company)
    db.commit()
    db.refresh(company)
    return company


# Note: other_company fixture is defined in tests/fixtures/vendor_fixtures.py


@pytest.mark.unit
@pytest.mark.vendors
class TestVendorService:
    """Test suite for VendorService following the application's exception patterns."""

    def setup_method(self):
        """Setup method following the same pattern as admin service tests."""
        self.service = VendorService()

    # ==================== create_vendor Tests ====================

    def test_create_vendor_success(self, db, test_user, test_company):
        """Test successful vendor creation."""
        unique_id = str(uuid.uuid4())[:8]
        vendor_data = VendorCreate(
            company_id=test_company.id,
            vendor_code=f"NEWVENDOR_{unique_id}",
            subdomain=f"newvendor{unique_id.lower()}",
            name=f"New Test Vendor {unique_id}",
            description="A new test vendor",
        )

        vendor = self.service.create_vendor(db, vendor_data, test_user)
        db.commit()

        assert vendor is not None
        assert vendor.vendor_code == f"NEWVENDOR_{unique_id}".upper()
        assert vendor.company_id == test_company.id
        assert vendor.is_verified is False  # Regular user creates unverified vendor

    def test_create_vendor_admin_auto_verify(self, db, test_admin, admin_company):
        """Test admin creates verified vendor automatically."""
        unique_id = str(uuid.uuid4())[:8]
        vendor_data = VendorCreate(
            company_id=admin_company.id,
            vendor_code=f"ADMINVENDOR_{unique_id}",
            subdomain=f"adminvendor{unique_id.lower()}",
            name=f"Admin Test Vendor {unique_id}",
        )

        vendor = self.service.create_vendor(db, vendor_data, test_admin)
        db.commit()

        assert vendor.is_verified is True  # Admin creates verified vendor

    def test_create_vendor_duplicate_code(self, db, test_user, test_company, test_vendor):
        """Test vendor creation fails with duplicate vendor code."""
        vendor_data = VendorCreate(
            company_id=test_company.id,
            vendor_code=test_vendor.vendor_code,
            subdomain="duplicatesub",
            name="Duplicate Name",
        )

        with pytest.raises(VendorAlreadyExistsException) as exc_info:
            self.service.create_vendor(db, vendor_data, test_user)

        exception = exc_info.value
        assert exception.status_code == 409
        assert exception.error_code == "VENDOR_ALREADY_EXISTS"
        assert test_vendor.vendor_code.upper() in exception.message

    def test_create_vendor_missing_company_id(self, db, test_user):
        """Test vendor creation fails without company_id."""
        # VendorCreate requires company_id, so this should raise ValidationError
        # from Pydantic before reaching service
        with pytest.raises(Exception):  # Pydantic ValidationError
            VendorCreate(
                vendor_code="NOCOMPANY",
                subdomain="nocompany",
                name="No Company Vendor",
            )

    def test_create_vendor_unauthorized_user(self, db, test_user, other_company):
        """Test vendor creation fails when user doesn't own company."""
        unique_id = str(uuid.uuid4())[:8]
        vendor_data = VendorCreate(
            company_id=other_company.id,  # Not owned by test_user
            vendor_code=f"UNAUTH_{unique_id}",
            subdomain=f"unauth{unique_id.lower()}",
            name=f"Unauthorized Vendor {unique_id}",
        )

        with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
            self.service.create_vendor(db, vendor_data, test_user)

        exception = exc_info.value
        assert exception.status_code == 403
        assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"

    def test_create_vendor_invalid_company_id(self, db, test_user):
        """Test vendor creation fails with non-existent company."""
        unique_id = str(uuid.uuid4())[:8]
        vendor_data = VendorCreate(
            company_id=99999,  # Non-existent company
            vendor_code=f"BADCOMPANY_{unique_id}",
            subdomain=f"badcompany{unique_id.lower()}",
            name=f"Bad Company Vendor {unique_id}",
        )

        with pytest.raises(InvalidVendorDataException) as exc_info:
            self.service.create_vendor(db, vendor_data, test_user)

        exception = exc_info.value
        assert exception.status_code == 422
        assert exception.error_code == "INVALID_VENDOR_DATA"
        assert "company_id" in exception.details.get("field", "")

    # ==================== get_vendors Tests ====================

    def test_get_vendors_regular_user(
        self, db, test_user, test_vendor, inactive_vendor
    ):
        """Test regular user can only see active verified vendors and own vendors."""
        vendors, total = self.service.get_vendors(db, test_user, skip=0, limit=100)

        vendor_codes = [vendor.vendor_code for vendor in vendors]
        assert test_vendor.vendor_code in vendor_codes
        # Inactive vendor should not be visible to regular user
        assert inactive_vendor.vendor_code not in vendor_codes

    def test_get_vendors_admin_user(
        self, db, test_admin, test_vendor, inactive_vendor, verified_vendor
    ):
        """Test admin user can see all vendors with filters."""
        vendors, total = self.service.get_vendors(
            db, test_admin, active_only=False, verified_only=False
        )

        vendor_codes = [vendor.vendor_code for vendor in vendors]
        assert test_vendor.vendor_code in vendor_codes
        assert inactive_vendor.vendor_code in vendor_codes
        assert verified_vendor.vendor_code in vendor_codes

    def test_get_vendors_pagination(self, db, test_admin):
        """Test vendor pagination."""
        vendors, total = self.service.get_vendors(
            db, test_admin, skip=0, limit=5, active_only=False
        )

        assert len(vendors) <= 5

    def test_get_vendors_database_error(self, db, test_user, monkeypatch):
        """Test get vendors handles database errors gracefully."""

        def mock_query(*args):
            raise Exception("Database query failed")

        monkeypatch.setattr(db, "query", mock_query)

        with pytest.raises(ValidationException) as exc_info:
            self.service.get_vendors(db, test_user)

        exception = exc_info.value
        assert exception.error_code == "VALIDATION_ERROR"
        assert "Failed to retrieve vendors" in exception.message

    # ==================== get_vendor_by_code Tests ====================

    def test_get_vendor_by_code_owner_access(self, db, test_user, test_vendor):
        """Test vendor owner can access their own vendor."""
        vendor = self.service.get_vendor_by_code(
            db, test_vendor.vendor_code.lower(), test_user
        )

        assert vendor is not None
        assert vendor.id == test_vendor.id

    def test_get_vendor_by_code_admin_access(self, db, test_admin, test_vendor):
        """Test admin can access any vendor."""
        vendor = self.service.get_vendor_by_code(
            db, test_vendor.vendor_code.lower(), test_admin
        )

        assert vendor is not None
        assert vendor.id == test_vendor.id

    def test_get_vendor_by_code_not_found(self, db, test_user):
        """Test vendor not found raises proper exception."""
        with pytest.raises(VendorNotFoundException) as exc_info:
            self.service.get_vendor_by_code(db, "NONEXISTENT", test_user)

        exception = exc_info.value
        assert exception.status_code == 404
        assert exception.error_code == "VENDOR_NOT_FOUND"

    def test_get_vendor_by_code_access_denied(self, db, test_user, inactive_vendor):
        """Test regular user cannot access unverified vendor they don't own."""
        with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
            self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)

        exception = exc_info.value
        assert exception.status_code == 403
        assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"

    # ==================== get_vendor_by_id Tests ====================

    def test_get_vendor_by_id_success(self, db, test_vendor):
        """Test getting vendor by ID."""
        vendor = self.service.get_vendor_by_id(db, test_vendor.id)

        assert vendor is not None
        assert vendor.id == test_vendor.id
        assert vendor.vendor_code == test_vendor.vendor_code

    def test_get_vendor_by_id_not_found(self, db):
        """Test getting non-existent vendor by ID."""
        with pytest.raises(VendorNotFoundException) as exc_info:
            self.service.get_vendor_by_id(db, 99999)

        exception = exc_info.value
        assert exception.status_code == 404
        assert exception.error_code == "VENDOR_NOT_FOUND"

    # ==================== get_active_vendor_by_code Tests ====================

    def test_get_active_vendor_by_code_success(self, db, test_vendor):
        """Test getting active vendor by code (public access)."""
        vendor = self.service.get_active_vendor_by_code(db, test_vendor.vendor_code)

        assert vendor is not None
        assert vendor.id == test_vendor.id
        assert vendor.is_active is True

    def test_get_active_vendor_by_code_inactive(self, db, inactive_vendor):
        """Test getting inactive vendor fails."""
        with pytest.raises(VendorNotFoundException):
            self.service.get_active_vendor_by_code(db, inactive_vendor.vendor_code)

    def test_get_active_vendor_by_code_not_found(self, db):
        """Test getting non-existent vendor fails."""
        with pytest.raises(VendorNotFoundException):
            self.service.get_active_vendor_by_code(db, "NONEXISTENT")

    # ==================== toggle_verification Tests ====================

    def test_toggle_verification_verify(self, db, inactive_vendor):
        """Test toggling verification on."""
        original_verified = inactive_vendor.is_verified
        vendor, message = self.service.toggle_verification(db, inactive_vendor.id)
        db.commit()

        assert vendor.is_verified != original_verified
        assert "verified" in message.lower()

    def test_toggle_verification_unverify(self, db, verified_vendor):
        """Test toggling verification off."""
        vendor, message = self.service.toggle_verification(db, verified_vendor.id)
        db.commit()

        assert vendor.is_verified is False
        assert "unverified" in message.lower()

    def test_toggle_verification_not_found(self, db):
        """Test toggle verification on non-existent vendor."""
        with pytest.raises(VendorNotFoundException):
            self.service.toggle_verification(db, 99999)

    # ==================== toggle_status Tests ====================

    def test_toggle_status_deactivate(self, db, test_vendor):
        """Test toggling active status off."""
        vendor, message = self.service.toggle_status(db, test_vendor.id)
        db.commit()

        assert vendor.is_active is False
        assert "inactive" in message.lower()

    def test_toggle_status_activate(self, db, inactive_vendor):
        """Test toggling active status on."""
        vendor, message = self.service.toggle_status(db, inactive_vendor.id)
        db.commit()

        assert vendor.is_active is True
        assert "active" in message.lower()

    def test_toggle_status_not_found(self, db):
        """Test toggle status on non-existent vendor."""
        with pytest.raises(VendorNotFoundException):
            self.service.toggle_status(db, 99999)

    # ==================== set_verification / set_status Tests ====================

    def test_set_verification_to_true(self, db, inactive_vendor):
        """Test setting verification to true."""
        vendor, message = self.service.set_verification(db, inactive_vendor.id, True)
        db.commit()

        assert vendor.is_verified is True

    def test_set_verification_to_false(self, db, verified_vendor):
        """Test setting verification to false."""
        vendor, message = self.service.set_verification(db, verified_vendor.id, False)
        db.commit()

        assert vendor.is_verified is False

    def test_set_status_to_active(self, db, inactive_vendor):
        """Test setting status to active."""
        vendor, message = self.service.set_status(db, inactive_vendor.id, True)
        db.commit()

        assert vendor.is_active is True

    def test_set_status_to_inactive(self, db, test_vendor):
        """Test setting status to inactive."""
        vendor, message = self.service.set_status(db, test_vendor.id, False)
        db.commit()

        assert vendor.is_active is False

    # ==================== add_product_to_catalog Tests ====================

    def test_add_product_to_vendor_success(self, db, test_vendor, unique_product):
        """Test successfully adding product to vendor."""
        from models.database.marketplace_product import MarketplaceProduct

        # Re-query objects to avoid session issues
        vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
        mp = db.query(MarketplaceProduct).filter(
            MarketplaceProduct.id == unique_product.id
        ).first()

        product_data = ProductCreate(
            marketplace_product_id=mp.id,
            price=15.99,
            is_featured=True,
        )

        product = self.service.add_product_to_catalog(db, vendor, product_data)
        db.commit()

        assert product is not None
        assert product.vendor_id == vendor.id
        assert product.marketplace_product_id == mp.id

    def test_add_product_to_vendor_product_not_found(self, db, test_vendor):
        """Test adding non-existent product to vendor fails."""
        product_data = ProductCreate(
            marketplace_product_id=99999,  # Non-existent ID
            price=15.99,
        )

        with pytest.raises(MarketplaceProductNotFoundException) as exc_info:
            self.service.add_product_to_catalog(db, test_vendor, product_data)

        exception = exc_info.value
        assert exception.status_code == 404
        assert exception.error_code == "PRODUCT_NOT_FOUND"

    def test_add_product_to_vendor_already_exists(self, db, test_vendor, test_product):
        """Test adding product that's already in vendor fails."""
        # Re-query to get fresh instances
        from models.database.product import Product
        vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
        product = db.query(Product).filter(Product.id == test_product.id).first()

        product_data = ProductCreate(
            marketplace_product_id=product.marketplace_product_id,
            price=15.99,
        )

        with pytest.raises(ProductAlreadyExistsException) as exc_info:
            self.service.add_product_to_catalog(db, vendor, product_data)

        exception = exc_info.value
        assert exception.status_code == 409
        assert exception.error_code == "PRODUCT_ALREADY_EXISTS"

    # ==================== get_products Tests ====================

    def test_get_products_owner_access(self, db, test_user, test_vendor, test_product):
        """Test vendor owner can get vendor products."""
        # Re-query vendor to get fresh instance
        vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
        products, total = self.service.get_products(db, vendor, test_user)

        assert total >= 1
        assert len(products) >= 1

    def test_get_products_access_denied(self, db, test_user, inactive_vendor):
        """Test non-owner cannot access unverified vendor products."""
        # Re-query vendor to get fresh instance
        vendor = db.query(Vendor).filter(Vendor.id == inactive_vendor.id).first()
        with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
            self.service.get_products(db, vendor, test_user)

        exception = exc_info.value
        assert exception.status_code == 403
        assert exception.error_code == "UNAUTHORIZED_VENDOR_ACCESS"

    def test_get_products_with_filters(self, db, test_user, test_vendor, test_product):
        """Test getting vendor products with various filters."""
        # Re-query vendor to get fresh instance
        vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
        # Test active only filter
        products, total = self.service.get_products(
            db, vendor, test_user, active_only=True
        )
        assert all(p.is_active for p in products)

    # ==================== Helper Method Tests ====================

    def test_vendor_code_exists(self, db, test_vendor):
        """Test _vendor_code_exists helper method."""
        assert self.service._vendor_code_exists(db, test_vendor.vendor_code) is True
        assert self.service._vendor_code_exists(db, "NONEXISTENT") is False

    def test_can_access_vendor_admin(self, db, test_admin, test_vendor):
        """Test admin can always access vendor."""
        # Re-query vendor to get fresh instance
        vendor = db.query(Vendor).filter(Vendor.id == test_vendor.id).first()
        assert self.service._can_access_vendor(vendor, test_admin) is True

    def test_can_access_vendor_active_verified(self, db, test_user, verified_vendor):
        """Test any user can access active verified vendor."""
        # Re-query vendor to get fresh instance
        vendor = db.query(Vendor).filter(Vendor.id == verified_vendor.id).first()
        assert self.service._can_access_vendor(vendor, test_user) is True


@pytest.mark.unit
@pytest.mark.vendors
class TestVendorServiceExceptionDetails:
    """Additional tests focusing specifically on exception structure and details."""

    def setup_method(self):
        self.service = VendorService()

    def test_exception_to_dict_structure(self, db, test_user, test_vendor, test_company):
        """Test that exceptions can be properly serialized to dict for API responses."""
        vendor_data = VendorCreate(
            company_id=test_company.id,
            vendor_code=test_vendor.vendor_code,
            subdomain="duplicate",
            name="Duplicate",
        )

        with pytest.raises(VendorAlreadyExistsException) as exc_info:
            self.service.create_vendor(db, vendor_data, test_user)

        exception = exc_info.value
        exception_dict = exception.to_dict()

        # Verify structure matches expected API response format
        assert "error_code" in exception_dict
        assert "message" in exception_dict
        assert "status_code" in exception_dict
        assert "details" in exception_dict

        # Verify values
        assert exception_dict["error_code"] == "VENDOR_ALREADY_EXISTS"
        assert exception_dict["status_code"] == 409
        assert isinstance(exception_dict["details"], dict)

    def test_authorization_exception_user_details(self, db, test_user, inactive_vendor):
        """Test authorization exceptions include user context."""
        with pytest.raises(UnauthorizedVendorAccessException) as exc_info:
            self.service.get_vendor_by_code(db, inactive_vendor.vendor_code, test_user)

        exception = exc_info.value
        assert exception.details["vendor_code"] == inactive_vendor.vendor_code
        assert exception.details["user_id"] == test_user.id
        assert "Unauthorized access" in exception.message

    def test_not_found_exception_details(self, db, test_user):
        """Test not found exceptions include identifier details."""
        with pytest.raises(VendorNotFoundException) as exc_info:
            self.service.get_vendor_by_code(db, "NOTEXIST", test_user)

        exception = exc_info.value
        assert exception.status_code == 404
        assert exception.error_code == "VENDOR_NOT_FOUND"
