/**
 * @fileoverview OrdoJS Parser Tests - Comprehensive test suite for recursive descent parser
 */

import { describe, expect, it } from 'vitest';
import {
  ComponentAST,
  DirectiveType,
  ExpressionType,
  LifecycleType,
  StatementType,
  SyntaxError
} from '../types/index.js';
import { OrdoJSLexer } from './lexer.js';
import { OrdoJSParser } from './parser.js';

describe('OrdoJSParser', () => {
  let lexer: OrdoJSLexer;
  let parser: OrdoJSParser;

  const parseComponent = (source: string): ComponentAST => {
    lexer = new OrdoJSLexer(source, 'test.ordo');
    const tokens = lexer.tokenize();
    parser = new OrdoJSParser(tokens, {}, 'test.ordo');
    return parser.parse();
  };

  const expectParseError = (source: string): SyntaxError[] => {
    try {
      parseComponent(source);
      throw new Error('Expected parsing to fail');
    } catch (error) {
      if (error instanceof SyntaxError) {
        return [error];
      }
      throw error;
    }
  };

  describe('Component Parsing', () => {
    it('should parse minimal component', () => {
      const source = `
        component TestComponent {
          markup {
            <div>Hello World</div>
          }
        }
      `;

      const ast = parseComponent(source);

      expect(ast.component.name).toBe('TestComponent');
      expect(ast.component.type).toBe('Component');
      expect(ast.component.markupBlock).toBeDefined();
      expect(ast.component.clientBlock).toBeUndefined();
      expect(ast.component.serverBlock).toBeUndefined();
    });

    it('should parse component with props', () => {
      const source = `
        component TestComponent(title: string, count: number = 0) {
          markup {
            <div>{title}: {count}</div>
          }
        }
      `;

      const ast = parseComponent(source);

      expect(ast.component.props).toHaveLength(2);
      expect(ast.component.props[0].name).toBe('title');
      expect(ast.component.props[0].dataType.name).toBe('string');
      expect(ast.component.props[0].isRequired).toBe(true);

      expect(ast.component.props[1].name).toBe('count');
      expect(ast.component.props[1].dataType.name).toBe('number');
      expect(ast.component.props[1].isRequired).toBe(false);
      expect(ast.component.props[1].defaultValue).toBeDefined();
    });

    it('should parse component with all blocks', () => {
      const source = `
        component FullComponent {
          client {
            let count = 0;

            onMount() {
              console.log('mounted');
            }
          }

          server {
            public async getData(): Promise<string> {
              return 'data';
            }
          }

          markup {
            <div>
              <button onclick={count++}>Count: {count}</button>
            </div>
          }
        }
      `;

      const ast = parseComponent(source);

      expect(ast.component.clientBlock).toBeDefined();
      expect(ast.component.serverBlock).toBeDefined();
      expect(ast.component.markupBlock).toBeDefined();
    });

    it('should handle missing component keyword', () => {
      const source = `
        TestComponent {
          markup { <div>test</div> }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain("Expected 'component' keyword");
    });

    it('should handle missing component name', () => {
      const source = `
        component {
          markup { <div>test</div> }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain('Expected component name');
    });
  });

  describe('Client Block Parsing', () => {
    it('should parse reactive variables', () => {
      const source = `
        component TestComponent {
          client {
            let count = 0;
            const name = "test";
            let items: string[] = [];
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;

      expect(clientBlock.reactiveVariables).toHaveLength(3);

      const countVar = clientBlock.reactiveVariables[0];
      expect(countVar.name).toBe('count');
      expect(countVar.isConst).toBe(false);
      expect(countVar.initialValue.expressionType).toBe(ExpressionType.LITERAL);
      expect(countVar.initialValue.value).toBe(0);

      const nameVar = clientBlock.reactiveVariables[1];
      expect(nameVar.name).toBe('name');
      expect(nameVar.isConst).toBe(true);
      expect(nameVar.initialValue.value).toBe('test');

      const itemsVar = clientBlock.reactiveVariables[2];
      expect(itemsVar.name).toBe('items');
      expect(itemsVar.dataType.name).toBe('string');
      expect(itemsVar.dataType.isArray).toBe(true);
    });

    it('should parse lifecycle hooks', () => {
      const source = `
        component TestComponent {
          client {
            onMount() {
              console.log('mounted');
            }

            onUnmount() {
              console.log('unmounted');
            }

            onUpdate() {
              console.log('updated');
            }
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;

      expect(clientBlock.lifecycle).toHaveLength(3);
      expect(clientBlock.lifecycle[0].hookType).toBe(LifecycleType.ON_MOUNT);
      expect(clientBlock.lifecycle[1].hookType).toBe(LifecycleType.ON_UNMOUNT);
      expect(clientBlock.lifecycle[2].hookType).toBe(LifecycleType.ON_UPDATE);
    });

    it('should parse client functions', () => {
      const source = `
        component TestComponent {
          client {
            handleClick(): void {
              count++;
            }

            calculateTotal(items: number[]): number {
              return items.reduce((sum, item) => sum + item, 0);
            }
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;

      expect(clientBlock.functions).toHaveLength(2);

      const handleClick = clientBlock.functions[0];
      expect(handleClick.name).toBe('handleClick');
      expect(handleClick.returnType.name).toBe('void');
      expect(handleClick.parameters).toHaveLength(0);

      const calculateTotal = clientBlock.functions[1];
      expect(calculateTotal.name).toBe('calculateTotal');
      expect(calculateTotal.returnType.name).toBe('number');
      expect(calculateTotal.parameters).toHaveLength(1);
      expect(calculateTotal.parameters[0].name).toBe('items');
      expect(calculateTotal.parameters[0].dataType.name).toBe('number');
      expect(calculateTotal.parameters[0].dataType.isArray).toBe(true);
    });

    it('should handle invalid reactive variable syntax', () => {
      const source = `
        component TestComponent {
          client {
            let = 0;
          }
          markup { <div>test</div> }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain('Expected variable name');
    });
  });

  describe('Server Block Parsing', () => {
    it('should parse server functions', () => {
      const source = `
        component TestComponent {
          server {
            public async getData(): Promise<string> {
              return 'data';
            }

            private processData(input: string): string {
              return input.toUpperCase();
            }
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const serverBlock = ast.component.serverBlock!;

      expect(serverBlock.functions).toHaveLength(2);

      const getData = serverBlock.functions[0];
      expect(getData.name).toBe('getData');
      expect(getData.isPublic).toBe(true);
      expect(getData.returnType.name).toBe('Promise');

      const processData = serverBlock.functions[1];
      expect(processData.name).toBe('processData');
      expect(processData.isPublic).toBe(false);
      expect(processData.parameters).toHaveLength(1);
      expect(processData.parameters[0].name).toBe('input');
      expect(processData.parameters[0].dataType.name).toBe('string');
    });

    it('should handle missing function body', () => {
      const source = `
        component TestComponent {
          server {
            public getData(): string
          }
          markup { <div>test</div> }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain("Expected '{'");
    });
  });

  describe('Markup Block Parsing', () => {
    it('should parse HTML elements', () => {
      const source = `
        component TestComponent {
          markup {
            <div class="container">
              <h1>Title</h1>
              <p>Content</p>
              <img src="image.jpg" alt="Image" />
            </div>
          }
        }
      `;

      const ast = parseComponent(source);
      const markupBlock = ast.component.markupBlock;

      expect(markupBlock.elements).toHaveLength(1);

      const divElement = markupBlock.elements[0];
      expect(divElement.tagName).toBe('div');
      expect(divElement.attributes).toHaveLength(1);
      expect(divElement.attributes[0].name).toBe('class');
      expect(divElement.attributes[0].value).toBe('container');
      expect(divElement.children).toHaveLength(3);

      const imgElement = divElement.children[2] as any;
      expect(imgElement.tagName).toBe('img');
      expect(imgElement.isSelfClosing).toBe(true);
      expect(imgElement.isVoidElement).toBe(true);
    });

    it('should parse interpolations', () => {
      const source = `
        component TestComponent {
          markup {
            <div>
              <h1>{title}</h1>
              <p>Count: {count + 1}</p>
              <span>{user.name}</span>
            </div>
          }
        }
      `;

      const ast = parseComponent(source);
      const markupBlock = ast.component.markupBlock;

      expect(markupBlock.interpolations).toHaveLength(3);

      const titleInterpolation = markupBlock.interpolations[0];
      expect(titleInterpolation.expression.expressionType).toBe(ExpressionType.IDENTIFIER);
      expect(titleInterpolation.expression.identifier).toBe('title');

      const countInterpolation = markupBlock.interpolations[1];
      expect(countInterpolation.expression.expressionType).toBe(ExpressionType.BINARY);
      expect(countInterpolation.expression.operator).toBe('+');

      const userInterpolation = markupBlock.interpolations[2];
      expect(userInterpolation.expression.expressionType).toBe(ExpressionType.MEMBER);
    });

    it('should parse directives', () => {
      const source = `
        component TestComponent {
          markup {
            <input bind:value={inputValue} />
            <button on:click={handleClick}>Click me</button>
            <div on:mouseover={handleHover}>Hover</div>
          }
        }
      `;

      const ast = parseComponent(source);
      const markupBlock = ast.component.markupBlock;

      const inputElement = markupBlock.elements[0];
      expect(inputElement.attributes[0].isDirective).toBe(true);
      expect(inputElement.attributes[0].directiveType).toBe(DirectiveType.BIND);
      expect(inputElement.attributes[0].name).toBe('bind:value');

      const buttonElement = markupBlock.elements[1];
      expect(buttonElement.attributes[0].isDirective).toBe(true);
      expect(buttonElement.attributes[0].directiveType).toBe(DirectiveType.ON);
      expect(buttonElement.attributes[0].name).toBe('on:click');
    });

    it('should handle unclosed HTML tags', () => {
      const source = `
        component TestComponent {
          markup {
            <div>
              <p>Unclosed paragraph
            </div>
          }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain('Expected');
    });

    it('should handle mismatched closing tags', () => {
      const source = `
        component TestComponent {
          markup {
            <div>
              <p>Content</span>
            </div>
          }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain('Mismatched closing tag');
    });
  });

  describe('Expression Parsing', () => {
    it('should parse binary expressions', () => {
      const source = `
        component TestComponent {
          client {
            let result = a + b * c - d / e;
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;
      const variable = clientBlock.reactiveVariables[0];

      expect(variable.initialValue.expressionType).toBe(ExpressionType.BINARY);
      expect(variable.initialValue.operator).toBe('-');
      expect(variable.initialValue.left?.expressionType).toBe(ExpressionType.BINARY);
      expect(variable.initialValue.left?.operator).toBe('+');
    });

    it('should parse function calls', () => {
      const source = `
        component TestComponent {
          client {
            let result = Math.max(a, b, c);
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;
      const variable = clientBlock.reactiveVariables[0];

      expect(variable.initialValue.expressionType).toBe(ExpressionType.CALL);
      expect(variable.initialValue.callee?.expressionType).toBe(ExpressionType.MEMBER);
      expect(variable.initialValue.arguments).toHaveLength(3);
    });

    it('should parse member access', () => {
      const source = `
        component TestComponent {
          client {
            let name = user.profile.name;
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;
      const variable = clientBlock.reactiveVariables[0];

      expect(variable.initialValue.expressionType).toBe(ExpressionType.MEMBER);
      expect(variable.initialValue.object?.expressionType).toBe(ExpressionType.MEMBER);
    });

    it('should parse assignment expressions', () => {
      const source = `
        component TestComponent {
          client {
            handleClick() {
              count = count + 1;
            }
          }
          markup { <div>test</div> }
        }
      `;

      const ast = parseComponent(source);
      const clientBlock = ast.component.clientBlock!;
      const func = clientBlock.functions[0];
      const statement = func.body[0];

      expect(statement.statementType).toBe(StatementType.EXPRESSION);
      expect(statement.expression?.expressionType).toBe(ExpressionType.ASSIGNMENT);
      expect(statement.expression?.operator).toBe('=');
    });

    it('should handle invalid expressions', () => {
      const source = `
        component TestComponent {
          client {
            let result = + * 5;
          }
          markup { <div>test</div> }
        }
      `;

      const errors = expectParseError(source);
      expect(errors[0].message).toContain('Expected expression');
    });
  });

  describe('Error Recovery', () => {
    it('should recover from multiple syntax errors', () => {
      const source = `
        component TestComponent {
          client {
            let = 0;  // Missing variable name
            const count;  // Missing initializer
            invalidFunction() // Missing body
          }
          markup {
            <div>
              <p>Unclosed paragraph
            </div>
          }
        }
      `;

      try {
        lexer = new OrdoJSLexer(source, 'test.ordo');
        const tokens = lexer.tokenize();
        parser = new OrdoJSParser(tokens, { allowRecovery: true, maxErrors: 10 }, 'test.ordo');
        parser.parse();
      } catch (error) {
        // Should collect multiple errors
        const errors = parser.getErrors();
        expect(errors.length).toBeGreaterThan(1);
      }
    });

    it('should limit error count', () => {
      const source = `
        component TestComponent {
          client {
            let;
            const;
            var;
            function;
            class;
          }
          markup { <div>test</div> }
        }
      `;

      try {
        lexer = new OrdoJSLexer(source, 'test.ordo');
        const tokens = lexer.tokenize();
        parser = new OrdoJSParser(tokens, { allowRecovery: true, maxErrors: 2 }, 'test.ordo');
        parser.parse();
      } catch (error) {
        const errors = parser.getErrors();
        expect(errors.length).toBeLessThanOrEqual(2);
      }
    });
  });

  describe('Complex Component Parsing', () => {
    it('should parse a realistic component', () => {
      const source = `
        component TodoApp(initialTodos: Todo[] = []) {
          client {
            let todos = initialTodos;
            let newTodo = "";
            let filter = "all";

            onMount() {
              loadTodos();
            }

            addTodo(): void {
              if (newTodo.trim()) {
                todos = [...todos, {
                  id: Date.now(),
                  text: newTodo.trim(),
                  completed: false
                }];
                newTodo = "";
              }
            }

            toggleTodo(id: number): void {
              todos = todos.map(todo =>
                todo.id === id ? { ...todo, completed: !todo.completed } : todo
              );
            }

            removeTodo(id: number): void {
              todos = todos.filter(todo => todo.id !== id);
            }
          }

          server {
            public async loadTodos(): Promise<Todo[]> {
              return await db.todos.findMany();
            }

            public async saveTodo(todo: Todo): Promise<Todo> {
              return await db.todos.create({ data: todo });
            }
          }

          markup {
            <div class="todo-app">
              <header>
                <h1>Todo App</h1>
                <input
                  bind:value={newTodo}
                  on:keydown={e => e.key === 'Enter' && addTodo()}
                  placeholder="Add a new todo..."
                />
                <button on:click={addTodo}>Add</button>
              </header>

              <main>
                <ul class="todo-list">
                  {#each todos as todo}
                    <li class={todo.completed ? 'completed' : ''}>
                      <input
                        type="checkbox"
                        bind:checked={todo.completed}
                        on:change={() => toggleTodo(todo.id)}
                      />
                      <span>{todo.text}</span>
                      <button on:click={() => removeTodo(todo.id)}>×</button>
                    </li>
                  {/each}
                </ul>
              </main>

              <footer>
                <span>{todos.filter(t => !t.completed).length} items left</span>
                <div class="filters">
                  <button
                    class={filter === 'all' ? 'active' : ''}
                    on:click={() => filter = 'all'}
                  >All</button>
                  <button
                    class={filter === 'active' ? 'active' : ''}
                    on:click={() => filter = 'active'}
                  >Active</button>
                  <button
                    class={filter === 'completed' ? 'active' : ''}
                    on:click={() => filter = 'completed'}
                  >Completed</button>
                </div>
              </footer>
            </div>
          }
        }
      `;

      const ast = parseComponent(source);

      // Verify component structure
      expect(ast.component.name).toBe('TodoApp');
      expect(ast.component.props).toHaveLength(1);
      expect(ast.component.clientBlock).toBeDefined();
      expect(ast.component.serverBlock).toBeDefined();
      expect(ast.component.markupBlock).toBeDefined();

      // Verify client block
      const clientBlock = ast.component.clientBlock!;
      expect(clientBlock.reactiveVariables).toHaveLength(3);
      expect(clientBlock.lifecycle).toHaveLength(1);
      expect(clientBlock.functions).toHaveLength(3);

      // Verify server block
      const serverBlock = ast.component.serverBlock!;
      expect(serverBlock.functions).toHaveLength(2);
      expect(serverBlock.functions[0].isPublic).toBe(true);
      expect(serverBlock.functions[1].isPublic).toBe(true);

      // Verify markup structure
      const markupBlock = ast.component.markupBlock;
      expect(markupBlock.elements).toHaveLength(1);
      expect(markupBlock.elements[0].tagName).toBe('div');
      expect(markupBlock.elements[0].attributes[0].name).toBe('class');
      expect(markupBlock.elements[0].attributes[0].value).toBe('todo-app');
    });
  });
});
