<?php

namespace Tests\Support;

use CodeIgniter\Model;
use CodeIgniter\Test\CIUnitTestCase;
use Config\Database;
use RuntimeException;

abstract class ModelCrudTestCase extends CIUnitTestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        DBReset::resetDatabase();
    }

    protected function assertModelCanInsertAndRetrieve(string $modelClass): void
    {
        $model = $this->newModel($modelClass);
        $this->assertModelFieldsExist($model);
        $record = $this->fakeModelRecord($model);
        $id = $this->primaryKeyValue($record, $model->primaryKey);

        $this->assertNotNull($id);

        $fetched = $model->find($id);

        $this->assertNotNull($fetched);
        $this->assertEquals($id, $this->recordValue($fetched, $model->primaryKey));
    }

    protected function assertModelCanUpdate(string $modelClass): void
    {
        $model = $this->newModel($modelClass);
        $this->assertModelFieldsExist($model);
        $record = $this->fakeModelRecord($model);
        $id = $this->primaryKeyValue($record, $model->primaryKey);
        $updateData = $this->updateData($model, $record);

        $this->assertNotFalse($model->update($id, $updateData));
    }

    protected function assertModelCanDelete(string $modelClass): void
    {
        $model = $this->newModel($modelClass);
        $this->assertModelFieldsExist($model);
        $record = $this->fakeModelRecord($model);
        $id = $this->primaryKeyValue($record, $model->primaryKey);

        $model->delete($id);

        $this->assertNull($model->find($id));
    }

    protected function primaryKeyValue(array|object $record, string $primaryKey): int|string
    {
        return $this->recordValue($record, $primaryKey);
    }

    protected function validSchoolYear(): string
    {
        $row = Database::connect('tests')
            ->table('school_years')
            ->select('name')
            ->orderBy('name', 'DESC')
            ->get()
            ->getRowArray();

        if ($row === null) {
            $year = $this->fallbackSchoolYear();
            $db = Database::connect('tests');
            $fields = $db->getFieldNames('school_years');
            $data = [
                'name' => $year,
                'status' => 'active',
            ];

            foreach (['activated_at', 'created_at', 'updated_at'] as $field) {
                if (in_array($field, $fields, true)) {
                    $data[$field] = date('Y-m-d H:i:s');
                }
            }

            $db->table('school_years')->insert($data);

            return $year;
        }

        return $row['name'];
    }

    protected function fakeModelRecord(Model $model): array|object
    {
        return fake($model, $this->fabricatorOverrides($model));
    }

    protected function assertModelFieldsExist(Model $model): void
    {
        $db = Database::connect('tests');

        $this->assertTrue(
            $db->tableExists($model->table, false),
            sprintf('%s maps to table "%s", but that table is not present in the migrated test schema.', $model::class, $model->table)
        );

        $databaseFields = $db->getFieldNames($model->table);

        foreach ($model->allowedFields as $field) {
            $this->assertContains(
                $field,
                $databaseFields,
                sprintf(
                    '%s allows field "%s", but table "%s" does not contain it.',
                    $model::class,
                    $field,
                    $model->table
                )
            );
        }
    }

    protected function fabricatorOverrides(Model $model): array
    {
        $overrides = [];

        if (in_array('school_year', $model->allowedFields, true)) {
            $overrides['school_year'] = $this->validSchoolYear();
        }

        foreach ($model->validationRules as $field => $rules) {
            if (! in_array($field, $model->allowedFields, true)) {
                continue;
            }

            $rules = is_array($rules) ? implode('|', $rules) : (string) $rules;

            if (preg_match('/(?:^|\|)in_list\[([^\]]+)\]/', $rules, $matches) === 1) {
                $overrides[$field] = explode(',', $matches[1])[0];
            } elseif (str_contains($rules, 'valid_email')) {
                $overrides[$field] = uniqid('model-test-', true) . '@example.test';
            } elseif (str_contains($rules, 'decimal')) {
                $overrides[$field] = '1.00';
            } elseif (str_contains($rules, 'integer')) {
                $overrides[$field] = 1;
            } elseif (str_contains($rules, 'valid_date[Y-m-d H:i:s]')) {
                $overrides[$field] = date('Y-m-d H:i:s');
            } elseif (str_contains($rules, 'valid_date')) {
                $overrides[$field] = date('Y-m-d');
            }
        }

        foreach ($model->allowedFields as $field) {
            if (array_key_exists($field, $overrides)) {
                continue;
            }

            if ($field === 'email') {
                $overrides[$field] = uniqid('model-test-', true) . '@example.test';
            } elseif ($field === 'status') {
                $overrides[$field] = 'Active';
            } elseif (str_ends_with($field, '_id')) {
                $overrides[$field] = 1;
            } elseif (str_starts_with($field, 'is_') || str_starts_with($field, 'has_')) {
                $overrides[$field] = 0;
            }
        }

        return $overrides;
    }

    protected function recordValue(array|object $record, string $field): int|string|null
    {
        if (is_array($record)) {
            return $record[$field] ?? null;
        }

        return $record->{$field} ?? null;
    }

    protected function updateData(Model $model, array|object $record): array
    {
        foreach ($model->allowedFields as $field) {
            if ($field === $model->primaryKey) {
                continue;
            }

            $rules = $model->validationRules[$field] ?? '';
            $rules = is_array($rules) ? implode('|', $rules) : (string) $rules;

            if (str_contains($rules, 'is_unique')) {
                continue;
            }

            $value = $this->recordValue($record, $field);

            if ($value !== null) {
                return [$field => $value];
            }
        }

        if ($model->useTimestamps && $model->updatedField !== '') {
            return [$model->updatedField => date('Y-m-d H:i:s')];
        }

        throw new RuntimeException(sprintf('No updateable data is available for %s.', $model::class));
    }

    private function newModel(string $modelClass): Model
    {
        $this->assertTrue(class_exists($modelClass), sprintf('Model %s is not part of the application.', $modelClass));

        $model = new $modelClass();

        if (! $model instanceof Model) {
            throw new RuntimeException(sprintf('%s is not a CodeIgniter model.', $modelClass));
        }

        return $model;
    }

    private function fallbackSchoolYear(): string
    {
        $year = (int) date('Y');

        return ($year - 1) . '-' . $year;
    }
}
