<?php

declare(strict_types=1);

namespace ApiModel\Tests\Integration\Http;

use ApiModel\Action\InventoryAction;
use ApiModel\Infrastructure\Persistence\Repositories\PdoInventoryRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoInventoryAdjustmentRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoInventoryTransferRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoJournalRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoChartAccountRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoExchangeRateRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoCompanyRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoBranchRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoUserAccessRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoProfileRepository;
use ApiModel\Infrastructure\Persistence\Repositories\PdoTaxRateRepository;
use ApiModel\Middleware\CompanyContextMiddleware;
use ApiModel\Middleware\ApiErrorHandler;
use ApiModel\Route\InventoryRoutes;
use ApiModel\Service\InventoryService;
use ApiModel\Service\InventoryAdjustmentService;
use ApiModel\Service\InventoryTransferService;
use ApiModel\Service\JournalService;
use ApiModel\Service\OrganizationService;
use Firebase\JWT\JWT;
use PDO;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;
use Slim\App;
use Slim\Factory\AppFactory;
use Slim\Psr7\Factory\ServerRequestFactory;
use Slim\Psr7\Factory\StreamFactory;

final class InventoryItemsRoutesTest extends TestCase
{
    private PDO $pdo;
    private App $app;
    private string $adminToken;

    private const ADMIN_USER_ID = '11111111-1111-1111-1111-111111111111';
    private const ADMIN_ROLE_ID = '33333333-3333-3333-3333-333333333333';
    private const COMPANY_ID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
    private const BRANCH_ID = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';

    protected function setUp(): void
    {
        parent::setUp();

        $this->pdo = new PDO('sqlite::memory:');
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $this->pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

        $this->createSchema();
        $this->seedData();
        $this->configureEnv();

        $this->app = $this->buildApp();
        $this->adminToken = $this->issueToken(self::ADMIN_USER_ID, 'ADMINISTRADOR');
    }

    private function companyHeaders(): array
    {
        return [
            'Authorization' => 'Bearer ' . $this->adminToken,
            'X-Company-Id' => self::COMPANY_ID,
        ];
    }

    public function testListCategoriesInitiallyEmpty(): void
    {
        $response = $this->request('GET', '/inventory/categories', $this->companyHeaders());
        $this->assertSame(200, $response->getStatusCode());
        $body = $this->decodeBody($response);
        $this->assertCount(0, $body);
    }

    public function testCategoryCrud(): void
    {
        // 1. Create Category
        $payload = [
            'name' => 'Oficina',
            'description' => 'Suministros de oficina'
        ];
        $response = $this->jsonRequest('POST', '/inventory/categories', $payload, $this->companyHeaders());
        $this->assertSame(200, $response->getStatusCode());
        $created = $this->decodeBody($response);
        $this->assertArrayHasKey('id', $created);
        $this->assertSame('Oficina', $created['name']);

        $categoryId = $created['id'];

        // 2. Get Category
        $getResponse = $this->request('GET', '/inventory/categories/' . $categoryId, $this->companyHeaders());
        $this->assertSame(200, $getResponse->getStatusCode());
        $fetched = $this->decodeBody($getResponse);
        $this->assertSame('Oficina', $fetched['name']);

        // 3. Edit Category
        $editPayload = [
            'name' => 'Oficina premium',
            'description' => 'Suministros de oficina alta calidad'
        ];
        $editResponse = $this->jsonRequest('PATCH', '/inventory/categories/' . $categoryId, $editPayload, $this->companyHeaders());
        $this->assertSame(200, $editResponse->getStatusCode());
        $edited = $this->decodeBody($editResponse);
        $this->assertSame('Oficina premium', $edited['name']);

        // 4. List Categories
        $listResponse = $this->request('GET', '/inventory/categories', $this->companyHeaders());
        $list = $this->decodeBody($listResponse);
        $this->assertCount(1, $list);
        $this->assertSame('Oficina premium', $list[0]['name']);

        // 5. Delete Category
        $delResponse = $this->request('DELETE', '/inventory/categories/' . $categoryId, $this->companyHeaders());
        $this->assertSame(200, $delResponse->getStatusCode());

        // 6. Confirm Deleted
        $confirmResponse = $this->request('GET', '/inventory/categories', $this->companyHeaders());
        $this->assertCount(0, $this->decodeBody($confirmResponse));
    }

    public function testItemCrud(): void
    {
        // 1. Create Tax rate and Chart Accounts first
        $this->pdo->exec("INSERT INTO tax_rates (id, company_id, name, rate, type) VALUES ('t-1', '" . self::COMPANY_ID . "', 'IVA 19%', 19.0, 'vat')");
        $this->pdo->exec("INSERT INTO chart_accounts (id, company_id, code, name, account_type) VALUES ('acc-inc', '" . self::COMPANY_ID . "', '4135', 'Ventas', 'revenue')");
        $this->pdo->exec("INSERT INTO chart_accounts (id, company_id, code, name, account_type) VALUES ('acc-exp', '" . self::COMPANY_ID . "', '5135', 'Compras', 'expense')");
        $this->pdo->exec("INSERT INTO chart_accounts (id, company_id, code, name, account_type) VALUES ('acc-inv', '" . self::COMPANY_ID . "', '1435', 'Inventario', 'asset')");

        // 2. Create Item
        $payload = [
            'sku' => 'PROD-001',
            'name' => 'Escritorio Madera',
            'description' => 'Escritorio de roble',
            'item_type' => 'product',
            'sale_price' => 150.0,
            'purchase_price' => 80.0,
            'tax_rate_id' => 't-1',
            'income_account_id' => 'acc-inc',
            'expense_account_id' => 'acc-exp',
            'inventory_account_id' => 'acc-inv',
            'is_active' => true,
        ];
        $response = $this->jsonRequest('POST', '/inventory/items', $payload, $this->companyHeaders());
        $this->assertSame(200, $response->getStatusCode());
        $created = $this->decodeBody($response);
        $this->assertArrayHasKey('id', $created);
        $this->assertSame('PROD-001', $created['sku']);
        $this->assertEquals(150.0, $created['sale_price']);

        $itemId = $created['id'];

        // 3. Edit Item
        $editPayload = [
            'name' => 'Escritorio Madera Premium',
            'sale_price' => 170.0,
        ];
        $editResponse = $this->jsonRequest('PATCH', '/inventory/items/' . $itemId, $editPayload, $this->companyHeaders());
        $this->assertSame(200, $editResponse->getStatusCode());
        $edited = $this->decodeBody($editResponse);
        $this->assertSame('Escritorio Madera Premium', $edited['name']);
        $this->assertEquals(170.0, $edited['sale_price']);

        // 4. List Items
        $listResponse = $this->request('GET', '/inventory/items', $this->companyHeaders());
        $this->assertCount(1, $this->decodeBody($listResponse));

        // 5. Delete Item
        $delResponse = $this->request('DELETE', '/inventory/items/' . $itemId, $this->companyHeaders());
        $this->assertSame(200, $delResponse->getStatusCode());

        // 6. Confirm deleted
        $confirmResponse = $this->request('GET', '/inventory/items', $this->companyHeaders());
        $this->assertCount(0, $this->decodeBody($confirmResponse));
    }

    public function testWarehouseCrud(): void
    {
        // 1. Create Warehouse
        $payload = [
            'code' => 'W-CENTRAL',
            'name' => 'Bodega Central',
            'address' => 'Zona Industrial 45',
            'is_active' => true,
        ];
        $response = $this->jsonRequest('POST', '/inventory/warehouses', $payload, $this->companyHeaders());
        $this->assertSame(200, $response->getStatusCode());
        $created = $this->decodeBody($response);
        $this->assertArrayHasKey('id', $created);
        $this->assertSame('W-CENTRAL', $created['code']);

        $warehouseId = $created['id'];

        // 2. Edit Warehouse
        $editPayload = [
            'name' => 'Bodega Central Principal',
        ];
        $editResponse = $this->jsonRequest('PATCH', '/inventory/warehouses/' . $warehouseId, $editPayload, $this->companyHeaders());
        $this->assertSame(200, $editResponse->getStatusCode());
        $edited = $this->decodeBody($editResponse);
        $this->assertSame('Bodega Central Principal', $edited['name']);

        // 3. List Stock (Should return empty array initially)
        $stockResponse = $this->request('GET', "/inventory/warehouses/{$warehouseId}/stock", $this->companyHeaders());
        $this->assertSame(200, $stockResponse->getStatusCode());
        $this->assertCount(0, $this->decodeBody($stockResponse));

        // 4. Delete Warehouse
        $delResponse = $this->request('DELETE', '/inventory/warehouses/' . $warehouseId, $this->companyHeaders());
        $this->assertSame(200, $delResponse->getStatusCode());
    }

    private function buildApp(): App
    {
        $app = AppFactory::create();

        $orgRepo = new PdoCompanyRepository($this->pdo);
        $branchRepo = new PdoBranchRepository($this->pdo);
        $uaRepo = new PdoUserAccessRepository($this->pdo);
        $profRepo = new PdoProfileRepository($this->pdo);
        $orgService = new OrganizationService($orgRepo, $branchRepo, $uaRepo, $profRepo);
        
        $companyMiddleware = new CompanyContextMiddleware($orgService);

        $inventoryRepo = new PdoInventoryRepository($this->pdo);
        $adjustmentRepo = new PdoInventoryAdjustmentRepository($this->pdo);
        $transferRepo = new PdoInventoryTransferRepository($this->pdo);
        $journalRepo = new PdoJournalRepository($this->pdo);
        $taxRateRepo = new PdoTaxRateRepository($this->pdo);
        $chartAccountRepo = new PdoChartAccountRepository($this->pdo);
        $exchangeRateRepo = new PdoExchangeRateRepository($this->pdo);

        $journalService = new JournalService($journalRepo, $chartAccountRepo, $orgRepo, $branchRepo, $exchangeRateRepo);
        
        $inventoryService = new InventoryService($inventoryRepo);
        $adjustmentService = new InventoryAdjustmentService($adjustmentRepo, $inventoryRepo, $journalRepo, $journalService, $this->pdo);
        $transferService = new InventoryTransferService($transferRepo, $inventoryRepo, $this->pdo);

        $action = new InventoryAction($inventoryService, $adjustmentService, $transferService);

        $uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}';
        $app->group('', function ($g) use ($action, $uuid): void {
            InventoryRoutes::register($g, $action, $uuid);
        });

        $app->addRoutingMiddleware();
        $app->add($companyMiddleware);
        $errorMiddleware = $app->addErrorMiddleware(false, true, true);
        $errorMiddleware->setDefaultErrorHandler(ApiErrorHandler::create($app->getResponseFactory()));

        return $app;
    }

    private function request(string $method, string $path, array $headers = []): ResponseInterface
    {
        $requestFactory = new ServerRequestFactory();
        $request = $requestFactory->createServerRequest($method, $path);
        foreach ($headers as $name => $value) {
            $request = $request->withHeader($name, $value);
        }
        return $this->app->handle($request);
    }

    private function jsonRequest(string $method, string $path, array $payload, array $headers = []): ResponseInterface
    {
        $streamFactory = new StreamFactory();
        $requestFactory = new ServerRequestFactory();
        $body = $streamFactory->createStream((string) json_encode($payload, JSON_THROW_ON_ERROR));

        $request = $requestFactory
            ->createServerRequest($method, $path)
            ->withHeader('Content-Type', 'application/json')
            ->withBody($body);

        foreach ($headers as $name => $value) {
            $request = $request->withHeader($name, $value);
        }
        return $this->app->handle($request);
    }

    private function decodeBody(ResponseInterface $response): array
    {
        $raw = (string) $response->getBody();
        $data = json_decode($raw, true);
        return is_array($data) ? $data : [];
    }

    private function issueToken(string $userId, string $role): string
    {
        return JWT::encode([
            'sub' => $userId,
            'role' => $role,
            'lang' => 'es',
            'iat' => time(),
            'exp' => time() + 3600,
        ], (string) $_ENV['JWT_SECRET'], 'HS256');
    }

    private function createSchema(): void
    {
        $this->pdo->exec("ATTACH DATABASE ':memory:' AS auth");

        $this->pdo->exec(
            'CREATE TABLE auth.roles (
                id TEXT PRIMARY KEY,
                name_es TEXT NOT NULL UNIQUE,
                name_en TEXT NOT NULL UNIQUE,
                description_es TEXT,
                description_en TEXT,
                is_active INTEGER NOT NULL DEFAULT 1
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE auth.users (
                id TEXT PRIMARY KEY,
                name TEXT NOT NULL,
                email TEXT NOT NULL UNIQUE,
                password_hash TEXT NULL,
                language TEXT NOT NULL DEFAULT "es",
                is_active INTEGER NOT NULL DEFAULT 1
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE auth.role_user (
                role_id TEXT NOT NULL,
                user_id TEXT NOT NULL,
                PRIMARY KEY (role_id, user_id)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE companies (
                id TEXT PRIMARY KEY,
                legal_name TEXT NOT NULL,
                trade_name TEXT,
                tax_id TEXT NOT NULL UNIQUE,
                base_currency TEXT NOT NULL DEFAULT "USD",
                email TEXT,
                phone TEXT,
                address TEXT,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE branches (
                id TEXT PRIMARY KEY,
                company_id TEXT NOT NULL,
                name TEXT NOT NULL,
                code TEXT,
                address TEXT,
                phone TEXT,
                is_main INTEGER NOT NULL DEFAULT 0,
                operating_currency TEXT NOT NULL DEFAULT "USD",
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, code)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE user_access (
                id TEXT PRIMARY KEY,
                user_id TEXT NOT NULL,
                company_id TEXT NOT NULL,
                branch_id TEXT,
                can_manage INTEGER NOT NULL DEFAULT 0,
                is_default INTEGER NOT NULL DEFAULT 0,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (user_id, company_id)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE chart_accounts (
                id TEXT PRIMARY KEY,
                parent_id TEXT NULL,
                company_id TEXT NOT NULL,
                code TEXT NOT NULL,
                name TEXT NOT NULL,
                account_type TEXT NOT NULL,
                is_header INTEGER NOT NULL DEFAULT 0,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, code)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE tax_rates (
                id TEXT PRIMARY KEY,
                company_id TEXT NOT NULL REFERENCES companies(id),
                name TEXT NOT NULL,
                rate REAL NOT NULL,
                type TEXT NOT NULL,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, name)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE inventory_categories (
                id TEXT PRIMARY KEY,
                company_id TEXT NOT NULL REFERENCES companies(id),
                name TEXT NOT NULL,
                description TEXT NULL,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, name)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE inventory_items (
                id TEXT PRIMARY KEY,
                company_id TEXT NOT NULL REFERENCES companies(id),
                category_id TEXT NULL REFERENCES inventory_categories(id),
                sku TEXT NOT NULL,
                name TEXT NOT NULL,
                description TEXT NULL,
                item_type TEXT NOT NULL DEFAULT "product",
                sale_price REAL NOT NULL DEFAULT 0.0,
                purchase_price REAL NOT NULL DEFAULT 0.0,
                tax_rate_id TEXT NULL REFERENCES tax_rates(id),
                income_account_id TEXT NULL REFERENCES chart_accounts(id),
                expense_account_id TEXT NULL REFERENCES chart_accounts(id),
                inventory_account_id TEXT NULL REFERENCES chart_accounts(id),
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, sku)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE warehouses (
                id TEXT PRIMARY KEY,
                company_id TEXT NOT NULL REFERENCES companies(id),
                name TEXT NOT NULL,
                code TEXT NOT NULL,
                address TEXT NULL,
                is_active INTEGER NOT NULL DEFAULT 1,
                created_at TEXT DEFAULT CURRENT_TIMESTAMP,
                updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
                UNIQUE (company_id, code)
            )'
        );

        $this->pdo->exec(
            'CREATE TABLE inventory_stock (
                item_id TEXT NOT NULL REFERENCES inventory_items(id),
                warehouse_id TEXT NOT NULL REFERENCES warehouses(id),
                quantity REAL NOT NULL DEFAULT 0.0,
                PRIMARY KEY (item_id, warehouse_id)
            )'
        );
    }

    private function seedData(): void
    {
        $this->pdo->prepare(
            'INSERT INTO auth.users (id, name, email, password_hash, is_active) VALUES (?,?,?, "hash", 1)'
        )->execute([self::ADMIN_USER_ID, 'Admin User', 'admin@example.com']);

        $this->pdo->prepare(
            'INSERT INTO auth.roles (id, name_es, name_en) VALUES (?,?,?)'
        )->execute([self::ADMIN_ROLE_ID, 'ADMINISTRADOR', 'ADMINISTRATOR']);

        $this->pdo->prepare('INSERT INTO auth.role_user (role_id, user_id) VALUES (?,?)')
            ->execute([self::ADMIN_ROLE_ID, self::ADMIN_USER_ID]);

        $this->pdo->prepare(
            'INSERT INTO companies (id, legal_name, trade_name, tax_id, base_currency, is_active)
             VALUES (?,?,?,?,?,1)'
        )->execute([self::COMPANY_ID, 'Empresa Demo', 'Demo', '900111222-3', 'USD']);

        $this->pdo->prepare(
            'INSERT INTO branches (id, company_id, name, code, is_main, operating_currency, is_active)
             VALUES (?,?,?,?,1,?,1)'
        )->execute([self::BRANCH_ID, self::COMPANY_ID, 'Principal', 'MAIN', 'USD']);

        $this->pdo->prepare(
            'INSERT INTO user_access (id, user_id, company_id, branch_id, can_manage, is_default)
             VALUES (?,?,?,?,1,1)'
        )->execute(['cccccccc-cccc-cccc-cccc-cccccccccccc', self::ADMIN_USER_ID, self::COMPANY_ID, self::BRANCH_ID]);
    }

    private function configureEnv(): void
    {
        $_ENV['JWT_SECRET'] = 'test-jwt-secret';
        $_ENV['JWT_EXPIRES_IN'] = '15m';
        $_ENV['JWT_REFRESH_EXPIRES_DAYS'] = '7';
        $_ENV['JWT_KID'] = '';
        $_ENV['JWT_SECRET_PREVIOUS'] = '';
    }
}
