<?php

namespace Tests\Unit\Framework;

use Openguru\OpenCartFramework\QueryBuilder\Builder;
use Openguru\OpenCartFramework\QueryBuilder\Connections\MySqlConnection;
use Openguru\OpenCartFramework\QueryBuilder\Grammars\MySqlGrammar;
use Openguru\OpenCartFramework\QueryBuilder\JoinClause;
use Openguru\OpenCartFramework\QueryBuilder\RawExpression;
use Openguru\OpenCartFramework\QueryBuilder\Table;
use Tests\TestCase;

class BuilderTest extends TestCase
{
    protected $builder;

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

        $connection = new MySqlConnection($this->getPdoMock());

        $this->builder = new Builder($connection, new MySqlGrammar());
    }

    public function testSelect(): void
    {
        $sql = $this->builder
            ->select([
                'foo',
                'boo' => 'bar',
            ])
            ->from('some_table')
            ->toSql();

        $this->assertEquals(/** @lang text */ 'SELECT foo, boo AS bar FROM some_table', $sql);
    }

    public function testSelectRawExpression(): void
    {
        $sql = $this->builder
            ->select([
                'foo',
                new RawExpression('IF (foobar IS NULL, TRUE, FALSE) AS alias'),
            ])
            ->from('some_table')
            ->toSql();

        $this->assertEquals(
        /** @lang text */ 'SELECT foo, IF (foobar IS NULL, TRUE, FALSE) AS alias FROM some_table',
            $sql
        );
    }

    public function testFrom(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table AS alias',
            $this->builder
                ->from('some_table', 'alias')
                ->toSql()
        );

        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table',
            $this->builder
                ->from('some_table')
                ->toSql()
        );
    }

    public function testWhereNotNull(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo IS NOT NULL',
            $this->builder
                ->from('some_table')
                ->whereNotNull('foo')
                ->toSql()
        );
    }

    public function testWhereNull(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo IS NULL',
            $this->builder
                ->from('some_table')
                ->whereNull('foo')
                ->toSql()
        );
    }

    public function testWhere(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo = ?',
            $this->builder
                ->from('some_table')
                ->where('foo', '=', 'bar')
                ->toSql()
        );

        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo LIKE ?',
            $this->builder
                ->newQuery()
                ->from('some_table')
                ->where('foo', 'LIKE', '%bar%')
                ->toSql()
        );
    }

    public function testOrWhere(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 WHERE foo = ? OR bar = ?',
            $this->builder
                ->from('t1')
                ->where('foo', '=', 'bar')
                ->orWhere('bar', '=', 'boo')
                ->toSql()
        );
    }

    public function testLimit(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table LIMIT 10',
            $this->builder
                ->from('some_table')
                ->limit(10)
                ->toSql()
        );
    }

    public function testOffset(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table OFFSET 10',
            $this->builder
                ->from('some_table')
                ->offset(10)
                ->toSql()
        );
    }

    public function testForPage(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table LIMIT 20 OFFSET 20',
            $this->builder
                ->from('some_table')
                ->forPage(2, 20)
                ->toSql()
        );
    }

    public function testOrderBy(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table ORDER BY foo ASC, bar DESC',
            $this->builder
                ->from('some_table')
                ->orderBy('foo')
                ->orderBy('bar', 'DESC')
                ->toSql()
        );
    }

    public function testJoin(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 INNER JOIN t2 ON t1.key = t2.key AND t1.foo IS NOT NULL OR t2.bar <> ?',
            $this->builder
                ->from('t1')
                ->join('t2', function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2.key')
                        ->whereNotNull('t1.foo')
                        ->where('t2.bar', '<>', 'value', 'or');
                })
                ->toSql()
        );
    }

    public function testLeftJoin(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 LEFT JOIN t2 ON t1.key = t2.key',
            $this->builder
                ->from('t1')
                ->join('t2', function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2.key');
                }, 'left')
                ->toSql()
        );

        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 LEFT JOIN t2 ON t1.key = t2.key',
            $this->builder->newQuery()
                ->from('t1')
                ->leftJoin('t2', function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2.key');
                })
                ->toSql()
        );
    }

    public function testWhereNested(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 WHERE (t1.foo = ? AND t2.bar = ?) OR (t1.foo = ? AND t2.bar = ?)',
            $this->builder
                ->from('t1')
                ->whereNested(function (Builder $builder) {
                    $builder->where('t1.foo', '=', 'bar')
                        ->where('t2.bar', '=', 'foo');
                })
                ->whereNested(function (Builder $builder) {
                    $builder->where('t1.foo', '=', 'foo')
                        ->where('t2.bar', '=', 'bar');
                }, 'or')
                ->toSql()
        );
    }

    public function testWhereNestedEmpty(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 WHERE ()',
            $this->builder
                ->from('t1')
                ->whereNested(function (Builder $builder) {
                })
                ->toSql()
        );
    }

    public function testWhereBetween(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo BETWEEN 100 AND 200',
            $this->builder
                ->from('some_table')
                ->whereBetween('foo', [100, 200])
                ->toRawSql()
        );
    }

    public function testWhereBetweenWithOperand(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo BETWEEN 100 AND 200 OR bar BETWEEN 10 AND 20',
            $this->builder
                ->from('some_table')
                ->whereBetween('foo', [100, 200])
                ->whereBetween('bar', [10, 20], 'or')
                ->toRawSql()
        );
    }

    public function testWhereIn(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo IN (1, 2, 3)',
            $this->builder
                ->from('some_table')
                ->whereIn('foo', [1, 2, 3])
                ->toRawSql()
        );
    }

    public function testWhereNotIn(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo NOT IN (1, 2, 3)',
            $this->builder
                ->from('some_table')
                ->whereIn('foo', [1, 2, 3], true)
                ->toRawSql()
        );

        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM some_table WHERE foo NOT IN (1, 2, 3)',
            $this->builder
                ->newQuery()
                ->from('some_table')
                ->whereNotIn('foo', [1, 2, 3])
                ->toRawSql()
        );
    }

    public function testDistinct(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT DISTINCT id FROM some_table',
            $this->builder
                ->select(['id'])
                ->distinct()
                ->from('some_table')
                ->toRawSql()
        );
    }

    public function testWhenConditionTrue(): void
    {
        $this->assertEquals(
        /** @lang text */ "SELECT id FROM some_table WHERE foo = 'bar'",
            $this->builder
                ->select(['id'])
                ->from('some_table')
                ->when(true, function (Builder $query) {
                    $query->where('foo', '=', 'bar');
                })
                ->toRawSql()
        );
    }

    public function testWhenConditionFalseWithDefault(): void
    {
        $this->assertEquals(
        /** @lang text */ "SELECT id FROM some_table WHERE foo <> 'bar'",
            $this->builder
                ->select(['id'])
                ->from('some_table')
                ->when(false, function (Builder $query) {
                    $query->where('foo', '=', 'bar');
                }, function (Builder $query) {
                    $query->where('foo', '<>', 'bar');
                })
                ->toRawSql()
        );
    }

    public function testWhenConditionFalseWithoutDefault(): void
    {
        $this->assertEquals(
        /** @lang text */ "SELECT id FROM some_table",
            $this->builder
                ->select(['id'])
                ->from('some_table')
                ->when(false, function (Builder $query) {
                    $query->where('foo', '=', 'bar');
                })
                ->toRawSql()
        );
    }

    public function testHasJoinAlias(): void
    {
        $this->builder
            ->select(['id'])
            ->from('table1')
            ->join('table2 AS tbl_alias', function (JoinClause $join) {
                return $join->on('table1.id', '=', 'tbl_alias.id');
            })
            ->toRawSql();

        $this->assertStringContainsString('AS tbl_alias', $this->builder->toRawSql());
    }

    public function testHasJoinAliasWithTableClass(): void
    {
        $this->builder
            ->select(['id'])
            ->from('table1')
            ->join(new Table('table2', 'tbl_alias'), function (JoinClause $join) {
                return $join->on('table1.id', '=', 'tbl_alias.id');
            })
            ->toRawSql();

        $this->assertTrue($this->builder->hasJoinAlias('tbl_alias'));
    }

    public function testHasJoinHasAliasWithTableClass(): void
    {
        $this->builder
            ->select(['id'])
            ->from('table1')
            ->join(new Table('table2', 'tbl_alias'), function (JoinClause $join) {
                return $join->on('table1.id', '=', 'tbl_alias.id');
            })
            ->toRawSql();

        $this->assertTrue($this->builder->hasJoinAlias('tbl_alias'));
    }

    public function testJoinWithAlias(): void
    {
        $this->assertEquals(
        /** @lang text */
            'SELECT * FROM t1 INNER JOIN t2 AS t2_alias ON t1.key = t2_alias.key AND t1.foo IS NOT NULL OR t2_alias.bar <> ?',
            $this->builder
                ->from('t1')
                ->join('t2 AS t2_alias', function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2_alias.key')
                        ->whereNotNull('t1.foo')
                        ->where('t2_alias.bar', '<>', 'value', 'or');
                })
                ->toSql()
        );
    }

    public function testJoinTableClassWithAlias(): void
    {
        $this->assertEquals(
        /** @lang text */
            'SELECT * FROM t1 INNER JOIN t2 AS t2_alias ON t1.key = t2_alias.key AND t1.foo IS NOT NULL OR t2_alias.bar <> ?',
            $this->builder
                ->from('t1')
                ->join(new Table('t2', 't2_alias'), function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2_alias.key')
                        ->whereNotNull('t1.foo')
                        ->where('t2_alias.bar', '<>', 'value', 'or');
                })
                ->toSql()
        );
    }

    public function testJoinTableClass(): void
    {
        $this->assertEquals(
        /** @lang text */ 'SELECT * FROM t1 INNER JOIN t2 ON t1.key = t2.key AND t1.foo IS NOT NULL OR t2.bar <> ?',
            $this->builder
                ->from('t1')
                ->join(new Table('t2'), function (JoinClause $join) {
                    $join->on('t1.key', '=', 't2.key')
                        ->whereNotNull('t1.foo')
                        ->where('t2.bar', '<>', 'value', 'or');
                })
                ->toSql()
        );
    }

    public function testJoinSub(): void
    {
        $sub = $this->builder->newQuery()
            ->select(['id', 'name'])
            ->from('table_2')
            ->limit(1);

        $query = $this->builder->newQuery()
            ->select([
                'table_1.id',
                'sub_alias.name',
            ])
            ->from('table_1')
            ->join(new Table($sub, 'sub_alias'), function (JoinClause $join) {
                $join->on('table_1.id', '=', 'sub_alias.id');
            });

        $expected = "SELECT table_1.id, sub_alias.name FROM table_1 INNER JOIN (SELECT id, name FROM table_2 LIMIT 1) AS sub_alias ON table_1.id = sub_alias.id";

        $this->assertEquals($expected, $query->toRawSql());
    }

    public function testLeftJoinSub(): void
    {
        $sub = $this->builder->newQuery()
            ->select(['id', 'name'])
            ->from('table_2')
            ->limit(1);

        $query = $this->builder->newQuery()
            ->select([
                'table_1.id',
                'sub_alias.name',
            ])
            ->from('table_1')
            ->leftJoin(new Table($sub, 'sub_alias'), function (JoinClause $join) {
                $join->on('table_1.id', '=', 'sub_alias.id');
            });

        $expected = "SELECT table_1.id, sub_alias.name FROM table_1 LEFT JOIN (SELECT id, name FROM table_2 LIMIT 1) AS sub_alias ON table_1.id = sub_alias.id";

        $this->assertEquals($expected, $query->toRawSql());
    }

    public function testJoinSubWithBindings(): void
    {
        $sub = $this->builder->newQuery()
            ->select(['id', 'name'])
            ->from('table_2')
            ->where('foo', '=', 'bar')
            ->limit(1);

        $query = $this->builder->newQuery()
            ->select([
                'table_1.id',
                'sub_alias.name',
            ])
            ->from('table_1')
            ->join(new Table($sub, 'sub_alias'), function (JoinClause $join) {
                $join->on('table_1.id', '=', 'sub_alias.id');
            });

        $expected = "SELECT table_1.id, sub_alias.name FROM table_1 INNER JOIN (SELECT id, name FROM table_2 WHERE foo = 'bar' LIMIT 1) AS sub_alias ON table_1.id = sub_alias.id";

        $this->assertEquals($expected, $query->toRawSql());
    }

    public function testGroupBy(): void
    {
        $query = $this->builder->newQuery()
            ->select(['foo', 'bar'])
            ->from('table_1')
            ->groupBy(['foo', 'bar']);

        $this->assertEquals(
            'SELECT foo, bar FROM table_1 GROUP BY foo, bar',
            $query->toRawSql(),
        );

    }

    public function testUpdate(): void
    {
        $connection = $this->createMock(MySqlConnection::class);
        $connection->expects($this->once())
            ->method('statement')
            ->with(
                'UPDATE `telecart_settings` SET `alias` = ?, `foo` = ? WHERE alias = ?',
                ['foobar2', 'bar2', 'foobar']
            )
            ->willReturn(true);

        $builder = new Builder($connection, new MySqlGrammar());

        $builder->newQuery()
            ->where('alias', '=', 'foobar')
            ->update('telecart_settings', [
                'alias' => 'foobar2',
                'foo' => 'bar2',
            ]);
    }

    public function testUpdateJsonField(): void
    {
        $json = json_encode(['xyz' => 'bazz'], JSON_THROW_ON_ERROR);

        $connection = $this->createMock(MySqlConnection::class);
        $connection->expects($this->once())
            ->method('statement')
            ->with(
                'UPDATE `telecart_settings` SET `json` = ? WHERE alias = ?',
                [$json, 'foobar']
            )
            ->willReturn(true);

        $builder = new Builder($connection, new MySqlGrammar());

        $builder->newQuery()
            ->where('alias', '=', 'foobar')
            ->update('telecart_settings', [
                'json' => $json,
            ]);
    }

    public function testUpdateJsonFieldWithCyrillic(): void
    {
        $json = json_encode(['xyz' => 'привет'], JSON_THROW_ON_ERROR);

        $connection = $this->createMock(MySqlConnection::class);
        $connection->expects($this->once())
            ->method('statement')
            ->with(
                'UPDATE `telecart_settings` SET `json` = ? WHERE alias = ?',
                [$json, 'foobar']
            )
            ->willReturn(true);

        $builder = new Builder($connection, new MySqlGrammar());

        $builder->newQuery()
            ->where('alias', '=', 'foobar')
            ->update('telecart_settings', [
                'json' => $json,
            ]);
    }

    public function testUpdateWithJoin(): void
    {
        $connection = $this->createMock(MySqlConnection::class);
        $connection->expects($this->once())
            ->method('statement')
            ->with(
                'UPDATE `t1` INNER JOIN t2 ON t1.id = t2.t1_id SET `t1.foo` = ? WHERE t2.bar = ?',
                ['new_value', 'condition']
            )
            ->willReturn(true);

        $builder = new Builder($connection, new MySqlGrammar());

        $builder->newQuery()
            ->join('t2', function (JoinClause $join) {
                $join->on('t1.id', '=', 't2.t1_id');
            })
            ->where('t2.bar', '=', 'condition')
            ->update('t1', [
                't1.foo' => 'new_value',
            ]);
    }
}