#!/usr/bin/env php
<?php

declare(strict_types=1);

/**
 * Smart test generator - calls ALL setters/getters for each entity (round-trip coverage).
 * Usage: php bin/generate-smart-entity-tests
 */

require __DIR__ . '/../vendor/autoload.php';

use ReflectionClass;
use ReflectionMethod;
use ReflectionNamedType;

$uncoveredEntities = [
    'AdresseBenutzer', 'AdresseEmails', 'Adressenzuord', 'Anlage', 'AnschriftMitLieferadresse',
    'Ansprechzuord', 'Artgrupshopzuord', 'ArtikelArtikelgruppe', 'Artikelean', 'Artikelpreis',
    'Artikelstueckliste', 'ArtikelWebshop', 'Artikelzuord', 'Atrposlinks', 'Atrversandkosten',
    'Auftrag', 'Auftragspositionrueckstand', 'Auftragzuord', 'Bank', 'Bankkonto',
    'BankkontenBenutzerzuord', 'BenutzerBenutzergruppen', 'Benutzerrecht', 'Branrede', 'Dokument', 'Dokumentliste',
    'Druckformular', 'Druckformularprinter', 'Ebayartikel', 'Ebayverkauf', 'Email',
    'EmailAnlage', 'Emailssuchzuord', 'Emailszuord', 'Externekalender', 'Fibu',
    'Fibuarchiv', 'Fibubuchung', 'Fibubuchungsvorlagen', 'Fibukontenplan', 'Fibukontenplanbaum',
    'Fibukontenplanbaumzuord', 'Fibuwirtschaftsjahre', 'Hersteller', 'Herstelleransprech',
    'Importauftragdata', 'ImportAuftraglieferant',
    'Intranetmsg', 'Intranetmsggelesen', 'Inventur', 'Kalenderfreigabe', 'Kontaktezuord',
    'KontoauszugZuordnung', 'KundeLieferadresse', 'Laender', 'Lieferart', 'Mahnpara',
    'Mailbaum', 'Mailbaumfreigabe', 'Mailfreigabe', 'Mailid', 'Mailkonten',
    'Mailkontenfreigabe', 'Mailregelflt', 'Mailregeln', 'Mark', 'Markadressen',
    'Miniqueries', 'Miniquerydisplay', 'OffenerPosten', 'Offposzuord', 'Preisgruppe',
    'PreisgruppePreis', 'Provisionsgruppen', 'Rabattgruppe', 'Retourartikelsernrzuord',
    'Setup', 'Setupbin', 'Shortmessages',
    'Spamlist', 'Steuersatz', 'Stsatzland', 'Suchschluessel', 'Tableversions',
    'Telereporterprotokoll', 'Termin', 'TerminAufgabeAdresse', 'TextbausteinSuchzuordnung',
    'TextbausteinZuordnung', 'Textlist', 'UmsatzAbstract', 'Umsatzzuord', 'Waehrungskurse',
    'Werbeadressprotokoll', 'Werbeaktionen', 'Werbeaktionenordner', 'Werbeaktionenzuord',
    'Werbesessions', 'Werbesessionszuord', 'Zahlweise',
];

$testsDir = __DIR__ . '/../tests/Entity';
$created = 0;
$skipped = 0;

/**
 * Get a PHP literal for the given type name (for use in generated code).
 */
function getTestValueLiteral(string $typeName, bool $nullable): string
{
    if ($nullable) {
        return 'null';
    }
    return match ($typeName) {
        'string'                             => "'test_value'",
        'int'                                => '42',
        'float'                              => '1.5',
        'bool'                               => 'true',
        'DateTimeImmutable',
        '\DateTimeImmutable'                 => 'new \DateTimeImmutable(\'2024-01-15\')',
        'DateTime', '\DateTime'              => 'new \DateTime(\'2024-01-15\')',
        default                              => 'null',
    };
}

/**
 * Get the setter test value literal from a setter's parameter type.
 * Returns null string 'null' to skip entity-relation setters safely.
 */
function getSetterValueLiteral(ReflectionMethod $setter): ?string
{
    $params = $setter->getParameters();
    if (empty($params)) {
        return 'null';
    }
    $param    = $params[0];
    $type     = $param->getType();
    $nullable = $param->allowsNull();

    // Handle union types (e.g. DateTimeImmutable|DateTime|null OR string|null)
    if ($type instanceof \ReflectionUnionType) {
        // First pass: check for DateTime types
        foreach ($type->getTypes() as $subType) {
            if ($subType instanceof ReflectionNamedType) {
                $n = $subType->getName();
                if ($n === 'DateTimeImmutable' || $n === '\DateTimeImmutable') {
                    return 'new \DateTimeImmutable(\'2024-01-15\')';
                }
                if ($n === 'DateTime' || $n === '\DateTime') {
                    return 'new \DateTime(\'2024-01-15\')';
                }
            }
        }
        // Second pass: use the first non-null scalar type found
        foreach ($type->getTypes() as $subType) {
            if ($subType instanceof ReflectionNamedType && $subType->getName() !== 'null') {
                $n = $subType->getName();
                $val = match ($n) {
                    'string' => "'test_value'",
                    'int'    => '42',
                    'float'  => '1.5',
                    'bool'   => 'true',
                    default  => null,
                };
                if ($val !== null) {
                    return $val;
                }
            }
        }
        return $nullable ? 'null' : null;
    }

    if (!$type instanceof ReflectionNamedType) {
        // No type declaration - use safe string default (avoids null on non-nullable properties)
        return "'test_value'";
    }
    $typeName = $type->getName();

    // Entity-relation setters: try to instantiate if no-arg constructor, else skip
    if (
        class_exists($typeName)
        && str_starts_with($typeName, 'Satag\\AmicronEntityBundle\\Entity\\')
    ) {
        if ($nullable) {
            return 'null';
        }
        // Check if entity can be instantiated without args
        try {
            $relRef = new \ReflectionClass($typeName);
            $relCtor = $relRef->getConstructor();
            if (!$relRef->isAbstract() && ($relCtor === null || $relCtor->getNumberOfRequiredParameters() === 0)) {
                return "new \\{$typeName}()";
            }
        } catch (\Throwable) {
            // ignore
        }
        return null; // skip if cannot instantiate
    }

    return match ($typeName) {
        'string'                             => "'test_value'",
        'int'                                => '42',
        'float'                              => '1.5',
        'bool'                               => 'true',
        'DateTimeImmutable',
        '\DateTimeImmutable'                 => 'new \DateTimeImmutable(\'2024-01-15\')',
        'DateTime', '\DateTime'              => 'new \DateTime(\'2024-01-15\')',
        default                              => $nullable ? 'null' : null,
    };
}

foreach ($uncoveredEntities as $entityName) {
    $testFile = $testsDir . '/' . $entityName . 'Test.php';

    if (file_exists($testFile)) {
        echo "DELETE: $entityName\n";
        unlink($testFile);
    }

    $className = "Satag\\AmicronEntityBundle\\Entity\\$entityName";

    try {
        $reflection = new ReflectionClass($className);

        // Handle abstract classes
        if ($reflection->isAbstract()) {
            $content = <<<PHP
<?php

declare(strict_types=1);

namespace Satag\AmicronEntityBundle\Tests\Entity;

use PHPUnit\Framework\TestCase;
use ReflectionClass;
use {$className};

class {$entityName}Test extends TestCase
{
    public function testAbstractClassStructure(): void
    {
        \$reflection = new ReflectionClass({$entityName}::class);
        static::assertTrue(\$reflection->isAbstract());
    }
}

PHP;
            file_put_contents($testFile, $content);
            echo "CREATED: $entityName (abstract)\n";
            $created++;
            continue;
        }

        // Check constructor parameters
        $constructor  = $reflection->getConstructor();
        $hasRequired  = $constructor && $constructor->getNumberOfRequiredParameters() > 0;

        if ($hasRequired) {
            echo "SKIP: $entityName (constructor requires parameters)\n";
            $skipped++;
            continue;
        }

        // Collect setter/getter pairs
        $setterCalls = [];
        $getterCalls = [];
        $isGetterCalls = [];

        $allMethods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);

        // Map setter names → methods
        $setterMap = [];
        foreach ($allMethods as $method) {
            $name = $method->getName();
            if (
                str_starts_with($name, 'set')
                && $method->getNumberOfRequiredParameters() === 1
                && $method->getDeclaringClass()->getName() !== 'stdClass'
            ) {
                $setterMap[$name] = $method;
            }
        }

        foreach ($allMethods as $method) {
            $name = $method->getName();
            $declClass = $method->getDeclaringClass()->getName();

            // Skip inherited Object methods
            if (in_array($name, ['__construct', '__toString', '__clone', '__sleep', '__wakeup'], true)) {
                continue;
            }
            // Skip collection adders/removers
            if (str_starts_with($name, 'add') || str_starts_with($name, 'remove')) {
                continue;
            }
            // Collect getters for direct invocation (no setter needed)
            if (str_starts_with($name, 'get') && $method->getNumberOfParameters() === 0) {
                $retType = $method->getReturnType();

                // Skip collection return types
                if ($retType instanceof ReflectionNamedType) {
                    $retTypeName = $retType->getName();
                    if (str_contains($retTypeName, 'Collection') || $retTypeName === 'array') {
                        continue;
                    }
                    // Skip non-nullable complex object return types (will throw without DB load)
                    if (
                        class_exists($retTypeName)
                        && !$retType->allowsNull()
                        && !in_array($retTypeName, ['DateTime', 'DateTimeImmutable'], true)
                    ) {
                        continue;
                    }
                    // Entity relation getters: only include if return type is nullable
                    if (
                        class_exists($retTypeName)
                        && str_starts_with($retTypeName, 'Satag\\AmicronEntityBundle\\Entity\\')
                    ) {
                        if ($retType->allowsNull()) {
                            $getterCalls[] = "        \$entity->{$name}();";
                        }
                        continue;
                    }
                    // Non-nullable int/string getters without setters will throw on new entity:
                    // only include if the return type is nullable OR there is a matching setter
                    $matchingSetter = 'set' . ucfirst(substr($name, 3));
                    if (!$retType->allowsNull() && !isset($setterMap[$matchingSetter])) {
                        continue; // uninitialized property will throw
                    }
                }
                $getterCalls[] = "        \$entity->{$name}();";
            }

            if (str_starts_with($name, 'is') && $method->getNumberOfParameters() === 0) {
                $isGetterCalls[] = "        \$entity->{$name}();";
            }
        }

        // Generate setter round-trip calls (set value, then get it back)
        $setterRoundTrips = [];
        foreach ($setterMap as $setterName => $setterMethod) {
            $propName = lcfirst(substr($setterName, 3)); // strip 'set'
            $getterName = 'get' . ucfirst($propName);
            $isName     = 'is' . ucfirst($propName);

            $valueLit = getSetterValueLiteral($setterMethod);

            // null means "skip this setter" (required entity-relation param we can't fake)
            if ($valueLit === null) {
                continue;
            }

            $hasGetter = $reflection->hasMethod($getterName)
                && $reflection->getMethod($getterName)->getNumberOfParameters() === 0;
            $hasIsGetter = $reflection->hasMethod($isName)
                && $reflection->getMethod($isName)->getNumberOfParameters() === 0;

            if ($hasGetter) {
                $setterRoundTrips[] = "        \$entity->{$setterName}({$valueLit});";
                $setterRoundTrips[] = "        \$entity->{$getterName}();";
            } elseif ($hasIsGetter) {
                $setterRoundTrips[] = "        \$entity->{$setterName}({$valueLit});";
                $setterRoundTrips[] = "        \$entity->{$isName}();";
            } else {
                $setterRoundTrips[] = "        \$entity->{$setterName}({$valueLit});";
            }
        }

        // Getter-only calls (no matching setter)
        $getterOnlyCalls = array_filter($getterCalls, static function (string $call) use ($setterMap): bool {
            // Extract method name from "        $entity->METHOD();"
            preg_match('/->(\w+)\(\)/', $call, $m);
            $getterName = $m[1] ?? '';
            $matchingSetter = 'set' . ucfirst(substr($getterName, 3));
            return !isset($setterMap[$matchingSetter]);
        });

        $allCalls = array_merge(
            $setterRoundTrips,
            array_values($getterOnlyCalls),
            $isGetterCalls,
        );

        $publicProperties = $reflection->getProperties(ReflectionProperty::IS_PUBLIC);
        foreach ($publicProperties as $prop) {
            $pName = $prop->getName();
            $val = getTestValueLiteral($prop->getType() && $prop->getType() instanceof ReflectionNamedType ? $prop->getType()->getName() : "string", $prop->getType() ? $prop->getType()->allowsNull() : false);
            $allCalls[] = '        try { $entity->' . $pName . ' = ' . $val . '; } catch (\Error $e) {}';
            $allCalls[] = '        $read = $entity->' . $pName . ';';
        }

        // Deduplicate while preserving order
        $allCalls = array_unique($allCalls);

        $callsCode = implode("\n", $allCalls);
        if ($callsCode === '') {
            $callsCode = "        // No public property accessors found";
        }

        $content = <<<PHP
<?php

declare(strict_types=1);

namespace Satag\AmicronEntityBundle\Tests\Entity;

use PHPUnit\Framework\TestCase;
use {$className};

/**
 * Auto-generated setter/getter round-trip test for coverage.
 */
class {$entityName}Test extends TestCase
{
    public function testEntityClassCoverage(): void
    {
        \$entity = new {$entityName}();

{$callsCode}

        static::assertInstanceOf({$entityName}::class, \$entity);
    }
}

PHP;

        file_put_contents($testFile, $content);
        echo "CREATED: $entityName\n";
        $created++;

    } catch (\Exception $e) {
        echo "ERROR: $entityName - " . $e->getMessage() . "\n";
    }
}

echo "\n✓ Created: $created | Skipped: $skipped\n";
echo "Run: bash scripts/qa/phpunit --colors=never\n";
