package p.studio.compiler.pbs.parser;

import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.ast.PbsAst;
import p.studio.compiler.pbs.lexer.PbsLexer;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;

import static org.junit.jupiter.api.Assertions.*;

class PbsParserStatementsTest {

    @Test
    void shouldParseLetConstAssignIfForWhileAndLoopControl() {
        final var source = """
                fn flow(n: int) -> int {
                  let const a: int = 1;
                  let b: int = n;
                  b += 2;
                  data.value = b;

                  if b > 0 {
                    b = b - 1;
                  } else if b == 0 {
                    b = 10;
                  } else {
                    b = 20;
                  }

                  for i: int from 0 until 10 step 2 {
                    if i == 4 {
                      break;
                    }
                    continue;
                  }

                  while b > 0 {
                    b -= 1;
                  }

                  return b;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var fileId = new FileId(0);
        final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);

        assertTrue(diagnostics.isEmpty(), "Valid statement/control-flow forms should parse without diagnostics");

        final var fn = ast.functions().getFirst();
        final var statements = fn.body().statements();

        final var letConst = assertInstanceOf(PbsAst.LetStatement.class, statements.get(0));
        assertTrue(letConst.isConst());

        final var assignCompound = assertInstanceOf(PbsAst.AssignStatement.class, statements.get(2));
        assertEquals(PbsAst.AssignOperator.ADD_ASSIGN, assignCompound.operator());

        final var assignPath = assertInstanceOf(PbsAst.AssignStatement.class, statements.get(3));
        assertEquals("data", assignPath.target().rootName());
        assertEquals(1, assignPath.target().pathSegments().size());

        assertInstanceOf(PbsAst.IfStatement.class, statements.get(4));
        assertInstanceOf(PbsAst.ForStatement.class, statements.get(5));
        assertInstanceOf(PbsAst.WhileStatement.class, statements.get(6));
        assertInstanceOf(PbsAst.ReturnStatement.class, statements.get(7));
    }

    @Test
    void shouldReportBreakContinueOutsideLoop() {
        final var source = """
                fn invalid() -> int {
                  break;
                  continue;
                  return 0;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var fileId = new FileId(0);
        final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);

        assertTrue(diagnostics.hasErrors(), "break/continue outside loops must report diagnostics");
        assertEquals(1, ast.functions().size());

        final var fn = ast.functions().getFirst();
        assertInstanceOf(PbsAst.BreakStatement.class, fn.body().statements().get(0));
        assertInstanceOf(PbsAst.ContinueStatement.class, fn.body().statements().get(1));
    }

    @Test
    void shouldReportInvalidForShape() {
        final var source = """
                fn badFor() -> int {
                  for i: int 0 until 10 {
                    return i;
                  }
                  return 0;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var fileId = new FileId(0);
        PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);

        assertTrue(diagnostics.hasErrors(), "Malformed for statement should produce deterministic diagnostics");
        assertTrue(diagnostics.stream().anyMatch(d -> d.getCode().equals(ParseErrors.E_PARSE_INVALID_FOR_FORM.name())));
    }

    @Test
    void shouldRejectCallInAssignmentTarget() {
        final var source = """
                fn badTarget() -> int {
                  obj.method() = 1;
                  return 0;
                }
                """;

        final var diagnostics = DiagnosticSink.empty();
        final var fileId = new FileId(0);
        PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);

        assertTrue(diagnostics.hasErrors(), "Assignment target with call segment must be rejected");
    }

    @Test
    void shouldParseThisAsAssignmentTargetRoot() {
        final var source = """
                declare struct Box(pub mut value: int) {
                  fn set(v: int) -> void {
                    this.value = v;
                    return;
                  }
                }
                """;
        final var diagnostics = DiagnosticSink.empty();
        final var fileId = new FileId(0);
        final var ast = PbsParser.parse(PbsLexer.lex(source, fileId, diagnostics), fileId, diagnostics);

        assertTrue(diagnostics.isEmpty(), "Assignment target rooted at 'this' should parse");

        final var structDecl = assertInstanceOf(PbsAst.StructDecl.class, ast.topDecls().getFirst());
        final var setMethod = structDecl.methods().getFirst();
        final var assignment = assertInstanceOf(PbsAst.AssignStatement.class, setMethod.body().statements().getFirst());
        assertEquals("this", assignment.target().rootName());
        assertEquals(1, assignment.target().pathSegments().size());
    }
}
