package p.studio.compiler.pbs.semantics;

import org.junit.jupiter.api.Test;
import p.studio.compiler.pbs.PbsFrontendCompiler;
import p.studio.compiler.source.diagnostics.DiagnosticSink;
import p.studio.compiler.source.identifiers.FileId;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class PbsSemanticsResultFlowRulesTest {

    @Test
    void shouldAcceptValidResultReturnAndHandleArmTerminals() {
        final var source = """
                declare error InErr { A; B; }
                declare error OutErr { Retry; Abort; }

                fn inner(v: int) -> result<InErr> int {
                  if v > 0 { return ok(v); }
                  return err(InErr.A);
                }

                fn wrap(v: int) -> result<OutErr> int {
                  let recovered: int = handle inner(v) {
                    InErr.A -> { ok(1) },
                    InErr.B -> { err(OutErr.Retry) }
                  };
                  return ok(recovered);
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);

        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name())));
    }

    @Test
    void shouldRejectOkAndErrOutsideAllowedResultFlowPositions() {
        final var source = """
                declare error Err { Fail; }

                fn bad(v: int) -> int {
                  ok(v);
                  err(Err.Fail);
                  return v;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_FLOW_INVALID_POSITION.name())));
    }

    @Test
    void shouldRejectMismatchedResultPayloadAndErrorLabels() {
        final var source = """
                declare error ErrA { Fail; }
                declare error ErrB { Stop; }

                fn badPayload() -> result<ErrA> int {
                  return ok("oops");
                }

                fn badLabel() -> result<ErrA> int {
                  return err(ErrB.Stop);
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
    }

    @Test
    void shouldRejectInvalidHandleBlockTerminalAndInvalidHandleTerminalForms() {
        final var source = """
                declare error InErr { Fail; }
                declare error OutErr { Retry; Abort; }
                declare error OtherErr { Oops; }

                fn source() -> result<InErr> int { return err(InErr.Fail); }

                fn invalidTerminal() -> result<OutErr> int {
                  let value: int = handle source() {
                    InErr.Fail -> { 1 }
                  };
                  return ok(value);
                }

                fn badHandleOkPayload() -> result<OutErr> int {
                  let value: int = handle source() {
                    InErr.Fail -> {
                      let payload: str = "x";
                      ok(payload)
                    }
                  };
                  return ok(value);
                }

                fn badHandleErrLabel() -> result<OutErr> int {
                  let value: int = handle source() {
                    InErr.Fail -> { err(OtherErr.Oops) }
                  };
                  return ok(value);
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

        new PbsFrontendCompiler().compileFile(new FileId(0), source, diagnostics);

        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_HANDLE_ARM_TERMINAL_INVALID.name())));
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_OK_PAYLOAD_MISMATCH.name())));
        assertTrue(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_RESULT_ERROR_LABEL_INVALID.name())));
    }
}
