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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

class PbsSemanticsAssignmentTest {

    @Test
    void shouldAllowLocalAndNestedFieldAssignments() {
        final var source = """
                declare struct Inner(pub mut value: int);
                declare struct Outer(pub mut inner: Inner) {
                  fn reset(v: int) -> void {
                    this.inner.value = v;
                    return;
                  }
                }

                fn update(o: Outer, next: int) -> int {
                  let current: int = 0;
                  current = next;
                  o.inner.value = current;
                  return current;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_WRITE_ACCESS_DENIED.name())));
    }

    @Test
    void shouldAllowGlobalAssignments() {
        final var source = """
                declare global tile_id: int = 0;

                fn tick(next: int) -> int {
                  tile_id = tile_id + next;
                  return tile_id;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name())));
        assertFalse(diagnostics.stream().anyMatch(d ->
                d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name())));
    }

    @Test
    void shouldRejectUnresolvedImmutableAndTypeMismatchedAssignments() {
        final var source = """
                fn bad(v: int) -> int {
                  let const fixed: int = 1;
                  fixed = v;
                  missing = v;
                  let n: int = 0;
                  n = true;
                  return n;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var unresolvedCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_UNRESOLVED.name()))
                .count();
        final var immutableCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TARGET_NOT_ASSIGNABLE.name()))
                .count();
        final var mismatchCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_ASSIGN_TYPE_MISMATCH.name()))
                .count();

        assertEquals(1, unresolvedCount);
        assertEquals(1, immutableCount);
        assertEquals(1, mismatchCount);
    }

    @Test
    void shouldEnforceFieldReadAndWriteAccessRules() {
        final var source = """
                declare struct Data(pub mut open: int, pub ro: int, hidden: int) {
                  fn own(v: int) -> int {
                    this.hidden = v;
                    this.ro = v;
                    return this.hidden + this.ro + this.open;
                  }
                }

                fn outside(d: Data) -> int {
                  let a: int = d.open;
                  let b: int = d.ro;
                  let c: int = d.hidden;
                  d.open = 1;
                  d.ro = 2;
                  d.hidden = 3;
                  return a + b + c;
                }
                """;
        final var diagnostics = DiagnosticSink.empty();

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

        final var readDeniedCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_READ_ACCESS_DENIED.name()))
                .count();
        final var writeDeniedCount = diagnostics.stream()
                .filter(d -> d.getCode().equals(PbsSemanticsErrors.E_SEM_FIELD_WRITE_ACCESS_DENIED.name()))
                .count();

        assertEquals(1, readDeniedCount);
        assertEquals(2, writeDeniedCount);
    }
}
