from datetime import date

from fastapi.testclient import TestClient
from sqlalchemy import create_engine, inspect, select, text
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool

from app.core.config import settings
from app.db.migrations import bootstrap_schema, sync_product_visibility, sync_tenant_ids
from app.db.session import Base
from app.main import app
from app.models.assumption import FreightCostRule, PackagingCostRule, ProcessCostRule
from app.models.client_access import ClientAccessAuditEvent, ClientAccount, ClientFeatureAccess, ClientUser
from app.schemas.mix_calculator import MixCalculatorSessionCreate
from app.models.mix import Mix, MixIngredient
from app.models.product import Product, ProductIngredient
from app.models.raw_material import RawMaterial, RawMaterialPriceVersion
from app.services.client_access_service import build_client_access_export, ensure_user_module_permissions, serialize_client_account
from app.services.costing_engine import calculate_mix_cost, calculate_product_cost, calculate_raw_material_cost, serialize_raw_material
from app.services.mix_calculator_service import build_mix_calculator_options, calculate_mix_calculator_preview


def build_session() -> Session:
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(bind=engine)
    session = sessionmaker(bind=engine, expire_on_commit=False)()
    return session


def test_calculate_raw_material_cost():
    raw_material = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    price = RawMaterialPriceVersion(market_value=500, waste_percentage=0.02, effective_date=date(2026, 4, 1))

    result = calculate_raw_material_cost(raw_material, price)

    assert result.loss_cost == 10.0
    assert result.cost_per_unit == 510.0
    assert result.cost_per_kg == 0.51


def test_serialize_raw_material_ignores_non_positive_active_price():
    raw_material = RawMaterial(name="Apple Flavouring", unit_of_measure="kg", kg_per_unit=1, status="active")
    raw_material.price_versions.append(RawMaterialPriceVersion(market_value=0, waste_percentage=0, effective_date=date(2026, 4, 1), status="active"))

    serialized = serialize_raw_material(raw_material)

    assert serialized["current_price"] is None


def test_mix_and_product_cost_breakdown():
    db = build_session()

    maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    maize.price_versions.append(RawMaterialPriceVersion(market_value=520, waste_percentage=0.02, effective_date=date(2026, 4, 1)))
    barley = RawMaterial(name="Barley", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    barley.price_versions.append(RawMaterialPriceVersion(market_value=470, waste_percentage=0.015, effective_date=date(2026, 4, 1)))
    db.add_all([maize, barley])
    db.flush()

    mix = Mix(client_name="Specialty Feeds", name="Pigeon Mix", status="active", version=1)
    db.add(mix)
    db.flush()
    db.add_all(
        [
            MixIngredient(mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
            MixIngredient(mix_id=mix.id, raw_material_id=barley.id, quantity_kg=100),
        ]
    )
    db.add(ProcessCostRule(process_name="standard_bagging", grading_cost=0.055, bagging_cost=0.04, cracking_cost=0.0))
    db.add(PackagingCostRule(sale_type="standard", unit_of_measure="20kg bag", own_bag=False, bag_cost=0.63))
    db.add(FreightCostRule(sale_type="standard", unit_of_measure="20kg bag", cost_per_unit=1.45))
    db.flush()
    product = Product(
        client_name="Specialty Feeds",
        name="Specialty Pigeon Breeder 20kg",
        mix_id=mix.id,
        sale_type="standard",
        own_bag=False,
        unit_of_measure="20kg bag",
        items_per_pallet=50,
        bagging_process="standard_bagging",
        distributor_margin=0.225,
        wholesale_margin=0.18,
    )
    db.add(product)
    db.commit()

    mix_result = calculate_mix_cost(db, mix.id)
    product_result = calculate_product_cost(db, product.id)

    assert mix_result["total_mix_kg"] == 280
    assert mix_result["mix_cost_per_kg"] == 0.5114
    assert product_result["client_name"] == "Specialty Feeds"
    assert product_result["mix_name"] == "Pigeon Mix"
    assert product_result["finished_product_delivered"] == 14.208
    assert product_result["distributor_price"] == 18.3329
    assert product_result["wholesale_price"] == 17.3268


def test_mix_calculator_preview_prefers_product_specific_ingredients_and_warns_on_fractional_bags():
    db = build_session()

    maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    barley = RawMaterial(name="Barley", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    wheat = RawMaterial(name="Wheat", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    db.add_all([maize, barley, wheat])
    db.flush()

    mix = Mix(tenant_id="specialty-feeds", client_name="Specialty Feeds", name="Pigeon Mix", status="active", version=1)
    db.add(mix)
    db.flush()
    db.add_all(
        [
            MixIngredient(tenant_id="specialty-feeds", mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
            MixIngredient(tenant_id="specialty-feeds", mix_id=mix.id, raw_material_id=barley.id, quantity_kg=100),
        ]
    )
    db.flush()

    product = Product(
        tenant_id="specialty-feeds",
        client_name="Specialty Feeds",
        name="Specialty Pigeon Breeder 20kg",
        mix_id=mix.id,
        sale_type="standard",
        own_bag=False,
        unit_of_measure="20kg bag",
        items_per_pallet=50,
        bagging_process="standard_bagging",
    )
    db.add(product)
    db.flush()
    db.add_all(
        [
            ProductIngredient(
                tenant_id="specialty-feeds",
                product_id=product.id,
                raw_material_id=maize.id,
                quantity_kg=300,
                sort_order=1,
            ),
            ProductIngredient(
                tenant_id="specialty-feeds",
                product_id=product.id,
                raw_material_id=wheat.id,
                quantity_kg=200,
                sort_order=2,
            ),
        ]
    )
    db.commit()

    preview = calculate_mix_calculator_preview(
        db,
        tenant_id="specialty-feeds",
        payload=MixCalculatorSessionCreate(
            mix_date=date(2026, 4, 29),
            client_name="Specialty Feeds",
            product_id=product.id,
            batch_size_kg=550,
            prepared_by_name="Shift A",
            notes="Mid-morning run",
        ),
    )

    assert preview["batch_size_kg"] == 550
    assert preview["total_bags"] == 27.5
    assert [line["raw_material_name"] for line in preview["lines"]] == ["Maize", "Wheat"]
    assert preview["lines"][0]["required_kg"] == 330
    assert preview["lines"][1]["required_kg"] == 220
    assert len(preview["warnings"]) == 1
    assert "not a whole-bag quantity" in preview["warnings"][0]


def test_mix_calculator_options_hide_invisible_products_and_clients():
    db = build_session()

    maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    barley = RawMaterial(name="Barley", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    db.add_all([maize, barley])
    db.flush()

    visible_mix = Mix(tenant_id="hunter-premium-produce", client_name="Peckish", name="Visible Mix", status="active", version=1)
    hidden_mix = Mix(tenant_id="hunter-premium-produce", client_name="Chaff", name="Hidden Mix", status="active", version=1)
    db.add_all([visible_mix, hidden_mix])
    db.flush()

    db.add_all(
        [
            MixIngredient(tenant_id="hunter-premium-produce", mix_id=visible_mix.id, raw_material_id=maize.id, quantity_kg=20),
            MixIngredient(tenant_id="hunter-premium-produce", mix_id=hidden_mix.id, raw_material_id=maize.id, quantity_kg=20),
        ]
    )
    db.flush()

    db.add_all(
        [
            Product(
                tenant_id="hunter-premium-produce",
                client_name="Peckish",
                name="Visible Product",
                mix_id=visible_mix.id,
                visible=True,
                sale_type="standard",
                own_bag=False,
                unit_of_measure="20kg bag",
                items_per_pallet=50,
            ),
            Product(
                tenant_id="hunter-premium-produce",
                client_name="Chaff",
                name="Hidden Product",
                mix_id=hidden_mix.id,
                visible=False,
                sale_type="standard",
                own_bag=False,
                unit_of_measure="20kg bag",
                items_per_pallet=50,
            ),
        ]
    )
    db.flush()

    visible_product = db.scalar(select(Product).where(Product.name == "Visible Product"))
    assert visible_product is not None
    db.add_all(
        [
            ProductIngredient(
                tenant_id="hunter-premium-produce",
                product_id=visible_product.id,
                raw_material_id=maize.id,
                quantity_kg=12,
                sort_order=1,
            ),
            ProductIngredient(
                tenant_id="hunter-premium-produce",
                product_id=visible_product.id,
                raw_material_id=barley.id,
                quantity_kg=8,
                sort_order=2,
            ),
        ]
    )
    db.commit()

    options = build_mix_calculator_options(db, tenant_id="hunter-premium-produce")

    assert options["clients"] == ["Peckish"]
    assert [product["product_name"] for product in options["products"]] == ["Visible Mix"]
    assert options["products"][0]["mix_total_kg"] == 20


def test_calculate_product_cost_prefers_product_specific_ingredients():
    db = build_session()

    maize = RawMaterial(name="Maize", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    maize.price_versions.append(RawMaterialPriceVersion(market_value=520, waste_percentage=0.02, effective_date=date(2026, 4, 1)))
    barley = RawMaterial(name="Barley", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    barley.price_versions.append(RawMaterialPriceVersion(market_value=470, waste_percentage=0.015, effective_date=date(2026, 4, 1)))
    wheat = RawMaterial(name="Wheat", unit_of_measure="tonne", kg_per_unit=1000, status="active")
    wheat.price_versions.append(RawMaterialPriceVersion(market_value=600, waste_percentage=0.01, effective_date=date(2026, 4, 1)))
    db.add_all([maize, barley, wheat])
    db.flush()

    mix = Mix(client_name="Specialty Feeds", name="Pigeon Mix", status="active", version=1)
    db.add(mix)
    db.flush()
    db.add_all(
        [
            MixIngredient(mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
            MixIngredient(mix_id=mix.id, raw_material_id=barley.id, quantity_kg=100),
        ]
    )
    db.add(ProcessCostRule(process_name="standard_bagging", grading_cost=0.055, bagging_cost=0.04, cracking_cost=0.0))
    db.add(PackagingCostRule(sale_type="standard", unit_of_measure="20kg bag", own_bag=False, bag_cost=0.63))
    db.add(FreightCostRule(sale_type="standard", unit_of_measure="20kg bag", cost_per_unit=1.45))
    db.flush()
    product = Product(
        client_name="Specialty Feeds",
        name="Specialty Pigeon Breeder 20kg",
        mix_id=mix.id,
        sale_type="standard",
        own_bag=False,
        unit_of_measure="20kg bag",
        items_per_pallet=50,
        bagging_process="standard_bagging",
        distributor_margin=0.225,
        wholesale_margin=0.18,
    )
    db.add(product)
    db.flush()
    db.add_all(
        [
            ProductIngredient(product_id=product.id, raw_material_id=maize.id, quantity_kg=300, sort_order=1),
            ProductIngredient(product_id=product.id, raw_material_id=wheat.id, quantity_kg=200, sort_order=2),
        ]
    )
    db.commit()

    product_result = calculate_product_cost(db, product.id)

    assert product_result["finished_product_delivered"] == 15.192
    assert product_result["distributor_price"] == 19.6026
    assert product_result["wholesale_price"] == 18.5268


def test_sync_product_visibility_hides_configured_clients():
    engine = create_engine("sqlite:///:memory:")
    with engine.begin() as connection:
        connection.execute(
            text(
                """
                CREATE TABLE products (
                    id INTEGER PRIMARY KEY,
                    client_name VARCHAR(255),
                    visible BOOLEAN NOT NULL DEFAULT TRUE
                )
                """
            )
        )
        connection.execute(
            text(
                """
                INSERT INTO products (id, client_name, visible)
                VALUES
                    (1, 'Chaff', TRUE),
                    (2, 'Peckish', TRUE),
                    (3, 'Uncategorized', TRUE)
                """
            )
        )

    updated = sync_product_visibility(engine)

    assert updated == 2
    with engine.connect() as connection:
        rows = connection.execute(text("SELECT client_name, visible FROM products ORDER BY id")).all()
    assert rows == [("Chaff", 0), ("Peckish", 1), ("Uncategorized", 0)]


def test_root_and_login_endpoints():
    with TestClient(app) as client:
        root_response = client.get("/")
        assert root_response.status_code == 200
        assert root_response.json()["endpoints"]["client_login"] == "/api/auth/client/login"
        assert root_response.json()["endpoints"]["admin_login"] == "/api/auth/admin/login"

        client_login_response = client.post(
            "/api/auth/client/login",
            json={"email": settings.client_email, "password": settings.client_password},
        )
        assert client_login_response.status_code == 200
        assert client_login_response.json()["email"] == settings.client_email
        assert client_login_response.json()["tenant_id"] == settings.client_tenant_id
        assert client_login_response.json()["client_role"] == "superadmin"
        assert client_login_response.json()["module_permissions"]["client_access"] == "manage"

        admin_login_response = client.post(
            "/api/auth/admin/login",
            json={"email": settings.admin_email, "password": settings.admin_password},
        )
        assert admin_login_response.status_code == 200
        assert admin_login_response.json()["email"] == settings.admin_email


def test_client_access_export_helpers():
    db = build_session()

    client = ClientAccount(
        tenant_id="specialty-feeds",
        name="Specialty Feeds",
        client_code="SPEC",
        status="active",
        powerbi_workspace="farm-ops-prod",
    )
    client.users.extend(
        [
            ClientUser(
                full_name="Amelia Hart",
                email="amelia.hart@specialtyfeeds.example",
                role="superadmin",
                status="active",
                is_new_user=False,
            ),
            ClientUser(
                full_name="Ethan Cole",
                email="ethan.cole@specialtyfeeds.example",
                role="operator",
                status="invited",
                is_new_user=True,
            ),
        ]
    )
    client.features.extend(
        [
            ClientFeatureAccess(
                feature_key="dashboard",
                feature_name="Dashboard",
                feature_group="workspace",
                enabled=True,
            ),
            ClientFeatureAccess(
                feature_key="products",
                feature_name="Products",
                feature_group="pricing",
                enabled=False,
            ),
        ]
    )
    db.add(client)
    db.flush()
    for user in client.users:
        ensure_user_module_permissions(db, user)
    client.audit_events.append(
        ClientAccessAuditEvent(
            tenant_id=client.tenant_id,
            actor_type="lean_admin",
            actor_name="Lean 101",
            actor_email="admin@lean101.local",
            actor_role="admin",
            action="client_access.seeded",
            target_type="client_account",
            target_id=client.id,
            module_key="client_access",
            summary="Initial client access controls were seeded.",
        )
    )
    db.commit()
    db.refresh(client)

    serialized = serialize_client_account(client)
    export = build_client_access_export([client])

    assert serialized["active_user_count"] == 1
    assert serialized["new_user_count"] == 1
    assert serialized["enabled_feature_count"] == 1
    assert export["client_rows"][0]["client_code"] == "SPEC"
    assert export["user_rows"][0]["client_name"] == "Specialty Feeds"
    assert len(export["feature_rows"]) == 2
    assert len(export["permission_rows"]) >= 1
    assert len(export["audit_rows"]) == 1


def test_client_access_endpoints():
    with TestClient(app) as client:
        login_response = client.post(
            "/api/auth/admin/login",
            json={"email": settings.admin_email, "password": settings.admin_password},
        )
        admin_cookies = {settings.admin_session_cookie_name: login_response.cookies.get(settings.admin_session_cookie_name)}

        access_response = client.get("/api/client-access", cookies=admin_cookies)
        assert access_response.status_code == 200
        assert len(access_response.json()) >= 1
        assert "audit_history" in access_response.json()[0]
        assert "module_permissions" in access_response.json()[0]["users"][0]

        export_response = client.get("/api/powerbi/client-access", cookies=admin_cookies)
        assert export_response.status_code == 200
        assert "client_rows" in export_response.json()
        assert "permission_rows" in export_response.json()

        client_login_response = client.post(
            "/api/auth/client/login",
            json={"email": settings.client_email, "password": settings.client_password},
        )
        client_cookies = {settings.session_cookie_name: client_login_response.cookies.get(settings.session_cookie_name)}
        superadmin_access_response = client.get("/api/client-access", cookies=client_cookies)
        assert superadmin_access_response.status_code == 200
        assert len(superadmin_access_response.json()) == 1


def test_mix_calculator_endpoints_respect_owner_visibility():
    with TestClient(app) as client:
        superadmin_login = client.post(
            "/api/auth/client/login",
            json={"email": settings.client_email, "password": settings.client_password},
        )
        assert superadmin_login.status_code == 200
        superadmin_cookies = {settings.session_cookie_name: superadmin_login.cookies.get(settings.session_cookie_name)}

        options_response = client.get("/api/mix-calculator/options", cookies=superadmin_cookies)
        assert options_response.status_code == 200
        options_payload = options_response.json()
        assert len(options_payload["products"]) == 84
        seeded_product = next(
            product
            for product in options_payload["products"]
            if product["client_name"] == "Specialty" and product["product_name"] == "Pigeon Mix"
        )
        assert seeded_product["unit_size_kg"] == 20

        create_response = client.post(
            "/api/mix-calculator",
            json={
                "mix_date": "2026-04-29",
                "client_name": seeded_product["client_name"],
                "product_id": seeded_product["product_id"],
                "batch_size_kg": 560,
                "prepared_by_name": "Amelia Hart",
                "notes": "Morning production run",
            },
            cookies=superadmin_cookies,
        )
        assert create_response.status_code == 201
        created = create_response.json()
        assert created["product_name"] == seeded_product["product_name"]
        assert created["session_number"].startswith("HPP-20260429-")
        assert created["total_bags"] == 28
        assert len(created["lines"]) > 0
        assert created["lines"][0]["required_kg"] > 0

        patch_response = client.patch(
            f"/api/mix-calculator/{created['id']}",
            json={"batch_size_kg": 550},
            cookies=superadmin_cookies,
        )
        assert patch_response.status_code == 200
        assert patch_response.json()["total_bags"] == 27.5
        assert len(patch_response.json()["warnings"]) == 1

        operator_login = client.post(
            "/api/auth/client/login",
            json={"email": "ethan.cole@hunterpremiumproduce.example", "password": settings.client_password},
        )
        assert operator_login.status_code == 200
        operator_cookies = {settings.session_cookie_name: operator_login.cookies.get(settings.session_cookie_name)}

        operator_list_response = client.get("/api/mix-calculator", cookies=operator_cookies)
        assert operator_list_response.status_code == 200
        assert operator_list_response.json() == []

        operator_detail_response = client.get(f"/api/mix-calculator/{created['id']}", cookies=operator_cookies)
        assert operator_detail_response.status_code == 404


def test_mix_calculator_pdf_endpoint_returns_pdf():
    with TestClient(app) as client:
        superadmin_login = client.post(
            "/api/auth/client/login",
            json={"email": settings.client_email, "password": settings.client_password},
        )
        superadmin_cookies = {settings.session_cookie_name: superadmin_login.cookies.get(settings.session_cookie_name)}

        options_response = client.get("/api/mix-calculator/options", cookies=superadmin_cookies)
        seeded_product = next(
            product
            for product in options_response.json()["products"]
            if product["client_name"] == "Specialty" and product["product_name"] == "Pigeon Mix"
        )

        create_response = client.post(
            "/api/mix-calculator",
            json={
                "mix_date": "2026-04-29",
                "client_name": seeded_product["client_name"],
                "product_id": seeded_product["product_id"],
                "batch_size_kg": 560,
                "prepared_by_name": "Amelia Hart",
                "notes": "Morning production run",
            },
            cookies=superadmin_cookies,
        )
        created = create_response.json()

        pdf_response = client.get(f"/api/mix-calculator/{created['id']}/pdf", cookies=superadmin_cookies)

        assert pdf_response.status_code == 200
        assert pdf_response.headers["content-type"] == "application/pdf"
        assert "attachment;" in pdf_response.headers["content-disposition"]
        assert pdf_response.content.startswith(b"%PDF")


def test_module_permission_blocks_client_module_access():
    with TestClient(app) as client:
        admin_login_response = client.post(
            "/api/auth/admin/login",
            json={"email": settings.admin_email, "password": settings.admin_password},
        )
        admin_cookies = {settings.admin_session_cookie_name: admin_login_response.cookies.get(settings.admin_session_cookie_name)}
        access_response = client.get("/api/client-access", cookies=admin_cookies)
        first_client = access_response.json()[0]
        first_user = next(user for user in first_client["users"] if user["email"] == settings.client_email)

        permission = next(
            permission for permission in first_user["module_permissions"] if permission["module_key"] == "raw_materials"
        )
        client.patch(
            f"/api/client-access/users/{first_user['id']}/module-permissions/{permission['module_key']}",
            json={"access_level": "none"},
            cookies=admin_cookies,
        )

        client_login_response = client.post(
            "/api/auth/client/login",
            json={"email": settings.client_email, "password": settings.client_password},
        )
        client_cookies = {settings.session_cookie_name: client_login_response.cookies.get(settings.session_cookie_name)}
        raw_materials_response = client.get("/api/raw-materials", cookies=client_cookies)

        assert raw_materials_response.status_code == 403


def test_bootstrap_schema_creates_missing_tables_and_patches_legacy_tenant_columns():
    engine = create_engine(
        "sqlite://",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )

    with engine.begin() as connection:
        connection.execute(
            text(
                """
                CREATE TABLE client_accounts (
                    id INTEGER PRIMARY KEY,
                    tenant_id VARCHAR(64),
                    name VARCHAR(255),
                    client_code VARCHAR(64),
                    status VARCHAR(32),
                    powerbi_workspace VARCHAR(128),
                    notes TEXT,
                    created_at DATETIME
                )
                """
            )
        )
        connection.execute(
            text(
                """
                CREATE TABLE raw_materials (
                    id INTEGER PRIMARY KEY,
                    name VARCHAR(255),
                    supplier VARCHAR(255),
                    unit_of_measure VARCHAR(64),
                    kg_per_unit FLOAT,
                    status VARCHAR(32),
                    notes TEXT,
                    created_at DATETIME
                )
                """
            )
        )
        connection.execute(
            text(
                """
                INSERT INTO client_accounts (id, tenant_id, name, client_code, status)
                VALUES (1, 'specialty-feeds', 'Specialty Feeds', 'SPEC', 'active')
                """
            )
        )
        connection.execute(
            text(
                """
                INSERT INTO raw_materials (id, name, supplier, unit_of_measure, kg_per_unit, status)
                VALUES (1, 'Maize', 'Example Supplier', 'tonne', 1000, 'active')
                """
            )
        )

    report = bootstrap_schema(engine, Base.metadata)
    synced_rows = sync_tenant_ids(engine)

    assert "products" in report.created_tables
    assert "raw_materials.tenant_id" in report.added_columns
    assert "tenant_id" in {column["name"] for column in inspect(engine).get_columns("raw_materials")}
    assert synced_rows["raw_materials"] == 1

    with engine.begin() as connection:
        tenant_id = connection.execute(text("SELECT tenant_id FROM raw_materials WHERE id = 1")).scalar_one()

    assert tenant_id == "specialty-feeds"
