<?php

namespace Livewire\Features\SupportModels;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Route;
use Livewire\Drawer\Utils;
use Livewire\Livewire;
use Sushi\Sushi;
use Tests\TestComponent;

class UnitTest extends \Tests\TestCase
{
    public function test_model_properties_are_persisted()
    {
        (new Article)::resolveConnection()->enableQueryLog();

        Livewire::test(new class extends \Livewire\Component {
            public Article $article;

            public function mount() {
                $this->article = Article::first();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        })
        ->assertSee('First')
        ->call('$refresh')
        ->assertSee('First');

        $this->assertCount(2, Article::resolveConnection()->getQueryLog());
    }

    public function test_cant_update_a_model_property()
    {
        $this->expectExceptionMessage("Can't set model properties directly");

        Livewire::test(new class extends \Livewire\Component {
            public Article $article;

            public function mount() {
                $this->article = Article::first();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        })
        ->assertSee('First')
        ->set('article.title', 'bar');
    }

    public function test_cant_view_model_data_in_javascript()
    {
        $data = Livewire::test(new class extends \Livewire\Component {
            public Article $article;

            public function mount() {
                $this->article = Article::first();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        })->getData();

        $this->assertNull($data['article']);
    }

    public function test_unpersisted_models_can_be_assigned_but_no_data_is_persisted_between_requests()
    {
        $component = Livewire::test(new class extends \Livewire\Component {
            public Article $article;

            public function mount() {
                $this->article = new Article();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        })
        ->call('$refresh')
        ->assertSet('article', new Article())
        ;

        $data = $component->getData();

        $this->assertNull($data['article']);
    }

    public function test_model_properties_are_lazy_loaded()
    {
        $this->markTestSkipped(); // @todo: probably not going to go this route...
        (new Article)::resolveConnection()->enableQueryLog();

        Livewire::test(new class extends TestComponent {
            #[Lazy]
            public Article $article;

            public function mount() {
                $this->article = Article::first();
            }

            public function save()
            {
                $this->article->save();
            }
        })
        ->call('$refresh')
        ->call('save');

        $this->assertCount(2, Article::resolveConnection()->getQueryLog());
    }


    public function test_it_uses_laravels_morph_map_instead_of_class_name_if_available_when_dehydrating()
    {
        Relation::morphMap([
            'article' => Article::class,
        ]);

        $component =  Livewire::test(ArticleComponent::class);

        $this->assertEquals('article', $component->snapshot['data']['article'][1]['class']);
    }

    public function test_it_uses_laravels_morph_map_instead_of_class_name_if_available_when_hydrating()
    {
        $article = Article::first();

        Relation::morphMap([
            'article' => Article::class,
        ]);

        Livewire::test(ArticleComponent::class)
            ->call('$refresh')
            ->assertSet('article', $article);
    }

    public function test_collections_with_duplicate_models_are_available_when_hydrating()
    {
        Livewire::test(new class extends \Livewire\Component {
            public Collection $articles;

            public function mount() {
                $this->articles = new Collection([
                    Article::first(),
                    Article::first(),
                ]);
            }

            public function render() { return <<<'HTML'
                <div>
                    @foreach($articles as $article)
                    {{ $article->title.'-'.$loop->index }}
                    @endforeach
                </div>
            HTML; }
        })
        ->assertSee('First-0')
        ->assertSee('First-1')
        ->call('$refresh')
        ->assertSee('First-0')
        ->assertSee('First-1');
    }

    public function test_collections_retain_their_order_on_hydration()
    {
        Livewire::test(new class extends \Livewire\Component {
            public Collection $articles;

            public function mount() {
                $this->articles = Article::all()->reverse();
            }

            public function render() { return <<<'HTML'
                <div>
                    @foreach($articles as $article)
                    {{ $article->title.'-'.$loop->index }}
                    @endforeach
                </div>
            HTML; }
        })
        ->assertSee('Second-0')
        ->assertSee('First-1')
        ->call('$refresh')
        ->assertSee('Second-0')
        ->assertSee('First-1');
    }

    public function test_it_does_not_trigger_ClassMorphViolationException_when_morh_map_is_enforced()
    {
        // reset morph
        Relation::morphMap([], false);
        Relation::requireMorphMap();

        $component = Livewire::test(new class extends TestComponent {
            public $article;

            public function mount()
            {
                $this->article = Article::first();
            }
        });

        $this->assertEquals(Article::class, $component->snapshot['data']['article'][1]['class']);

        Relation::requireMorphMap(false);
    }

    public function test_it_does_not_trigger_ClassMorphViolationException_for_collections_when_morph_map_is_enforced()
    {
        // reset morph
        Relation::morphMap([], false);
        Relation::requireMorphMap();

        $component = Livewire::test(new class extends TestComponent {
            public Collection $articles;

            public function mount()
            {
                $this->articles = Article::all();
            }
        });

        $this->assertEquals(Article::class, $component->snapshot['data']['articles'][1]['modelClass']);

        Relation::requireMorphMap(false);
    }

    public function test_it_dehydrates_models_with_overridden_getMorphClass_using_actual_class()
    {
        $component = Livewire::test(new class extends \Livewire\Component {
            public $article;

            public function mount() {
                $this->article = ExtendedArticle::first();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        });

        $this->assertEquals(ExtendedArticle::class, $component->snapshot['data']['article'][1]['class']);
    }

    public function test_it_hydrates_models_with_overridden_getMorphClass_using_actual_class()
    {
        Livewire::test(new class extends \Livewire\Component {
            public $article;

            public function mount() {
                $this->article = ExtendedArticle::first();
            }

            public function render() { return <<<'HTML'
                <div>{{ $article->title }}</div>
            HTML; }
        })
        ->call('$refresh')
        ->assertSet('article', ExtendedArticle::first());
    }

    public function test_model_synth_rejects_non_model_classes()
    {
        $this->expectException(\Exception::class);
        $this->expectExceptionMessage('Invalid model class');

        $component = Livewire::test(ArticleComponent::class);

        // Create a synth instance and try to hydrate with a non-Model class
        $synth = new ModelSynth(
            new \Livewire\Mechanisms\HandleComponents\ComponentContext($component->instance()),
            'article'
        );

        // This should throw because stdClass doesn't extend Model
        $synth->hydrate(null, ['class' => \stdClass::class]);
    }

    public function test_route_bound_models_are_not_queried_twice_on_update()
    {
        Route::livewire('/test-articles/{article}', RouteModelBindingComponent::class)
            ->middleware('web');

        // GET the page to mount the component and get a valid snapshot
        $response = $this->withoutExceptionHandling()->get('/test-articles/1');
        $response->assertOk();
        $response->assertSee('First');

        // Extract snapshot from the rendered HTML
        $html = $response->getContent();
        $snapshot = Utils::extractAttributeDataFromHtml($html, 'wire:snapshot');
        $encodedSnapshot = json_encode($snapshot);

        // Flush state from the initial render
        app('livewire')->flushState();

        // Enable query log and clear it
        Article::resolveConnection()->enableQueryLog();
        Article::resolveConnection()->flushQueryLog();

        // POST to the update endpoint to trigger an update request
        $this->withHeaders(['X-Livewire' => 'true'])
            ->postJson(app('livewire')->getUpdateUri(), [
                'components' => [
                    [
                        'snapshot' => $encodedSnapshot,
                        'calls' => [['method' => '$refresh', 'params' => [], 'path' => '']],
                        'updates' => [],
                    ],
                ],
            ])->assertOk();

        // Count queries to the articles table — should only be one, not two
        $queryLog = Article::resolveConnection()->getQueryLog();
        $articleQueries = array_filter($queryLog, fn ($q) => str_contains($q['query'], 'articles'));

        $this->assertCount(1, array_values($articleQueries));
    }

    public function test_hydrating_an_empty_eloquent_collection_does_not_trigger_deprecations()
    {
        $component = Livewire::test(new class extends \Livewire\Component {
            public Collection $articles;

            public function mount() {
                $this->articles = new Collection();
            }

            public function render() { return <<<'HTML'
                <div>count: {{ count($articles) }}</div>
            HTML; }
        });

        // An empty collection dehydrates with `modelClass` as `null`. Convert
        // deprecations to exceptions so the test fails if hydrating passes
        // that `null` as an array offset (deprecated in PHP 8.5)...
        set_error_handler(function ($severity, $message) {
            throw new \ErrorException($message, 0, $severity);
        }, E_DEPRECATED | E_USER_DEPRECATED);

        try {
            $component->call('$refresh')->assertSee('count: 0');
        } finally {
            restore_error_handler();
        }
    }
}

#[\Attribute]
class Lazy {
    //
}

class ArticleComponent extends \Livewire\Component
{
    public $article;

    public function mount()
    {
        $this->article = Article::first();
    }

    public function render()
    {
        return '<div>{{ $article->title }}</div>';
    }
}

class Article extends Model
{
    use Sushi;

    protected $rows = [
        ['title' => 'First'],
        ['title' => 'Second'],
    ];
}

// Simulates tightenco/parental's HasParent trait, which overrides
// getTable() and getMorphClass() to return the parent's values so
// that polymorphic relations use the parent's table.
class ExtendedArticle extends Article
{
    protected $table = 'articles';

    public function getMorphClass()
    {
        return (new Article)->getMorphClass();
    }
}

class RouteModelBindingComponent extends \Livewire\Component
{
    public Article $article;

    public function render()
    {
        return '<div>{{ $article->title }}</div>';
    }
}
