import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/src/utils/ast_equality.dart';
import 'package:test/test.dart';

import '../utils.dart';

void _enforceFrom(SelectStatement stmt, Queryable expected) {
  enforceHasSpan(stmt);
  enforceEqual(stmt.from!, expected);
}

void main() {
  group('from', () {
    test('a simple table', () {
      final stmt =
          SqlEngine().parse('SELECT * FROM tbl').rootNode as SelectStatement;

      _enforceFrom(stmt, TableReference('tbl'));
    });

    test('schema name and alias', () {
      final stmt = SqlEngine().parse('SELECT * FROM main.tbl foo').rootNode
          as SelectStatement;
      _enforceFrom(stmt, TableReference('tbl', schemaName: 'main', as: 'foo'));
    });

    test('from more than one table', () {
      final stmt = SqlEngine()
          .parse('SELECT * FROM tbl AS test, table2')
          .rootNode as SelectStatement;

      _enforceFrom(
        stmt,
        JoinClause(
          primary: TableReference('tbl', as: 'test'),
          joins: [
            Join(
              operator: JoinOperator.comma(),
              query: TableReference('table2'),
            ),
          ],
        ),
      );
    });

    test('more than one table with ON constraint', () {
      final stmt = SqlEngine()
          .parse('SELECT * FROM tbl AS test, table2 ON TRUE')
          .rootNode as SelectStatement;

      _enforceFrom(
        stmt,
        JoinClause(
          primary: TableReference('tbl', as: 'test'),
          joins: [
            Join(
              operator: JoinOperator.comma(),
              query: TableReference('table2'),
              constraint: OnConstraint(
                expression: BooleanLiteral(true),
              ),
            ),
          ],
        ),
      );
    });

    test('inner select statements', () {
      final stmt = SqlEngine()
          .parse(
              'SELECT * FROM table1, (SELECT * FROM table2 WHERE a) as "inner"')
          .rootNode as SelectStatement;

      _enforceFrom(
        stmt,
        JoinClause(
          primary: TableReference('table1'),
          joins: [
            Join(
              operator: JoinOperator.comma(),
              query: SelectStatementAsSource(
                statement: SelectStatement(
                  columns: [StarResultColumn(null)],
                  from: TableReference('table2'),
                  where: Reference(columnName: 'a'),
                ),
                as: 'inner',
              ),
            ),
          ],
        ),
      );
    });

    test('inner compound select statements', () {
      final stmt = SqlEngine()
          .parse('SELECT SUM(*) FROM (SELECT COUNT(*) FROM table1 UNION ALL '
              'SELECT COUNT(*) from table2)')
          .rootNode as SelectStatement;

      final countStar = ExpressionResultColumn(
        expression: FunctionExpression(
          name: 'COUNT',
          parameters: StarFunctionParameter(),
        ),
      );

      _enforceFrom(
        stmt,
        SelectStatementAsSource(
          statement: CompoundSelectStatement(
            base: SelectStatement(
              columns: [countStar],
              from: TableReference('table1'),
            ),
            additional: [
              CompoundSelectPart(
                mode: CompoundSelectMode.unionAll,
                select: SelectStatement(
                  columns: [countStar],
                  from: TableReference('table2'),
                ),
              ),
            ],
          ),
        ),
      );
    });

    test('from a join', () {
      final stmt = SqlEngine()
          .parse('SELECT * FROM table1 '
              'INNER JOIN table2 USING (test) '
              'LEFT OUTER JOIN table3 ON TRUE')
          .rootNode as SelectStatement;

      _enforceFrom(
        stmt,
        JoinClause(
          primary: TableReference('table1'),
          joins: [
            Join(
              operator: JoinOperator(JoinOperatorKind.inner),
              query: TableReference('table2'),
              constraint: UsingConstraint(columnNames: ['test']),
            ),
            Join(
              operator: JoinOperator(JoinOperatorKind.left, outer: true),
              query: TableReference('table3'),
              constraint: OnConstraint(expression: BooleanLiteral(true)),
            ),
          ],
        ),
      );
    });

    test('table valued function', () {
      testStatement(
        '''
SELECT DISTINCT user.name
  FROM user, json_each(user.phone)
WHERE json_each.value LIKE '704-%';
        ''',
        SelectStatement(
          distinct: true,
          columns: [
            ExpressionResultColumn(
              expression: Reference(entityName: 'user', columnName: 'name'),
            ),
          ],
          from: JoinClause(
            primary: TableReference('user'),
            joins: [
              Join(
                operator: JoinOperator.comma(),
                query: TableValuedFunction(
                  'json_each',
                  ExprFunctionParameters(parameters: [
                    Reference(entityName: 'user', columnName: 'phone')
                  ]),
                ),
              ),
            ],
          ),
          where: StringComparisonExpression(
            left: Reference(entityName: 'json_each', columnName: 'value'),
            operator: token(TokenType.like),
            right: StringLiteral('704-%'),
          ),
        ),
      );
    });
  });

  group('join kinds', () {
    SelectStatement selectWith(Queryable from) {
      return SelectStatement(
        columns: [StarResultColumn()],
        from: from,
      );
    }

    test('comma', () {
      testStatement(
        'SELECT * FROM foo, bar',
        selectWith(
          JoinClause(primary: TableReference('foo'), joins: [
            Join(operator: JoinOperator.comma(), query: TableReference('bar'))
          ]),
        ),
      );
    });

    test('none', () {
      testStatement(
        'SELECT * FROM foo JOIN bar',
        selectWith(
          JoinClause(primary: TableReference('foo'), joins: [
            Join(
                operator: JoinOperator(JoinOperatorKind.none),
                query: TableReference('bar'))
          ]),
        ),
      );
    });

    test('natural none', () {
      testStatement(
        'SELECT * FROM foo NATURAL JOIN bar',
        selectWith(
          JoinClause(primary: TableReference('foo'), joins: [
            Join(
                operator: JoinOperator(JoinOperatorKind.none, natural: true),
                query: TableReference('bar'))
          ]),
        ),
      );
    });

    test('cross', () {
      testStatement(
        'SELECT * FROM foo CROSS JOIN bar',
        selectWith(
          JoinClause(primary: TableReference('foo'), joins: [
            Join(
                operator: JoinOperator(JoinOperatorKind.cross),
                query: TableReference('bar'))
          ]),
        ),
      );
    });

    for (final natural in [true, false]) {
      group('with natural: $natural', () {
        final naturalText = natural ? 'NATURAL' : '';

        test('inner', () {
          testStatement(
            'SELECT * FROM foo $naturalText INNER JOIN bar',
            selectWith(
              JoinClause(primary: TableReference('foo'), joins: [
                Join(
                  operator: JoinOperator(
                    JoinOperatorKind.inner,
                    natural: natural,
                  ),
                  query: TableReference('bar'),
                )
              ]),
            ),
          );
        });

        for (final outer in [true, false]) {
          final outerText = outer ? 'OUTER' : '';
          const kinds = {
            'LEFT': JoinOperatorKind.left,
            'RIGHT': JoinOperatorKind.right,
            'FULL': JoinOperatorKind.full,
          };

          group('with outer: $outer', () {
            kinds.forEach((text, operatorKind) {
              test('operator $text', () {
                testStatement(
                  'SELECT * FROM foo $naturalText $text $outerText JOIN bar',
                  selectWith(
                    JoinClause(primary: TableReference('foo'), joins: [
                      Join(
                        operator: JoinOperator(
                          operatorKind,
                          natural: natural,
                          outer: outer,
                        ),
                        query: TableReference('bar'),
                      )
                    ]),
                  ),
                );
              });
            });
          });
        }
      });
    }

    test('does not allow natural with comma', () {
      expectParseError('SELECT * FROM foo NATURAL, bar', span: ',');
    });

    test('does not allow natural with cross', () {
      expectParseError('SELECT * FROM foo NATURAL CROSS JOIN bar',
          span: 'CROSS');
    });

    test('does not allow INNER OUTER', () {
      expectParseError('SELECT * FROM foo INNER OUTER CROSS JOIN bar',
          span: 'OUTER');
    });
  });
}
