package libraryloan;

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

import module java.base;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

public final class LibraryTest {

  private static final LocalDate JUNE_1 = LocalDate.of(2026, 6, 1);
  private static final LocalDate JUNE_2 = LocalDate.of(2026, 6, 2);
  private static final LocalDate JUNE_16 = LocalDate.of(2026, 6, 16);
  private static final LocalDate JUNE_17 = LocalDate.of(2026, 6, 17);
  private static final LocalDate JUNE_20 = LocalDate.of(2026, 6, 20);
  private static final LocalDate JUNE_21 = LocalDate.of(2026, 6, 21);


  @Nested
  public class Q1 {

    @Test
    public void invalidLoan() {
      assertAll(
          () -> assertThrows(NullPointerException.class,
              () -> new Loan(null, new Book("Book"), JUNE_1)),
          () -> assertThrows(NullPointerException.class,
              () -> new Loan("Alice", null, JUNE_1)),
          () -> assertThrows(NullPointerException.class,
              () -> new Loan("Alice", new Book("Book"), null))
      );
    }

    @Test
    public void emptyLibrary() {
      var library = new Library();

      assertEquals(List.of(), library.loans());
    }

    @Test
    public void addOneLoan() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);
      library.addLoan(loan);

      assertEquals(List.of(loan), library.loans());
    }

    @Test
    public void addSeveralLoans() {
      var library = new Library();
      var loan1 = new Loan("Alice", new Book("Book"), JUNE_1);
      var loan2 = new Loan("Bob", new Magazine("Magazine"), JUNE_1);
      var loan3 = new Loan("Alice", new Comic("Comic"), JUNE_1);
      library.addLoan(loan1);
      library.addLoan(loan2);
      library.addLoan(loan3);

      assertEquals(List.of(loan1, loan2, loan3), library.loans());
    }

    @Test
    public void addLoanNull() {
      var library = new Library();

      assertThrows(NullPointerException.class, () -> library.addLoan(null));
    }

    @Test
    public void loansCannotBeModified() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);
      library.addLoan(loan);

      var loans = library.loans();

      assertThrows(
          UnsupportedOperationException.class,
          () -> loans.add(new Loan("Bob", new Book("Book"), JUNE_1)));
    }

    @Test
    public void loansCannotBeCleared() {
      var library = new Library();
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      var loans = library.loans();

      assertThrows(
          UnsupportedOperationException.class,
          loans::clear);

      assertEquals(1, loans.size());
    }

    @Test
    public void addLargeLoans() {
      var library = new Library();
      for (var i = 0; i < 100_000; i++) {
        library.addLoan(new Loan("Alice " + i, new Book("Book" + i), JUNE_1));
      }

      for (var i = 0; i < 100_000; i++) {
        var loan = library.loans().get(i);
        assertEquals("Alice " + i, loan.member());
        assertEquals(new Book("Book" + i), loan.document());
        assertEquals(JUNE_1, loan.borrowedOn());
      }
    }

    @Test
    public void invalidBook() {
      assertThrows(NullPointerException.class, () -> new Book(null));
    }

    @Test
    public void invalidComic() {
      assertThrows(NullPointerException.class, () -> new Comic(null));
    }

    @Test
    public void invalidMagazine() {
      assertThrows(NullPointerException.class, () -> new Magazine(null));
    }

    @Test
    public void bookIsPublicAndImmutable() {
      assertTrue(Book.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(Book.class.accessFlags().contains(AccessFlag.FINAL));
      assertTrue(Arrays.stream(Book.class.getDeclaredFields())
          .allMatch(f -> f.accessFlags().contains(AccessFlag.FINAL)));
    }

    @Test
    public void comicIsPublicAndImmutable() {
      assertTrue(Comic.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(Comic.class.accessFlags().contains(AccessFlag.FINAL));
      assertTrue(Arrays.stream(Comic.class.getDeclaredFields())
          .allMatch(f -> f.accessFlags().contains(AccessFlag.FINAL)));
    }

    @Test
    public void magazineIsPublicAndImmutable() {
      assertTrue(Magazine.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(Magazine.class.accessFlags().contains(AccessFlag.FINAL));
      assertTrue(Arrays.stream(Magazine.class.getDeclaredFields())
          .allMatch(f -> f.accessFlags().contains(AccessFlag.FINAL)));
    }

    @Test
    public void libraryIsPublicAndCanNotBeInherited() {
      assertTrue(Library.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(Library.class.accessFlags().contains(AccessFlag.FINAL));
    }

    @Test
    public void libraryClassConstructorCallSuperAsLastInstruction() throws IOException {
      var className = "/" + Library.class.getName().replace('.', '/') + ".class";
      byte[] data;
      try(var input = Library.class.getResourceAsStream(className)) {
        if (input == null) {
          throw new AssertionError("Could not read class file");
        }
        data = input.readAllBytes();
      }
      var classModel = ClassFile.of().parse(data);
      var constructors =
          classModel.methods().stream().filter(m -> m.methodName().equalsString("<init>")).toList();
      for (var constructor : constructors) {
        var code =
            constructor.code().orElseThrow(() -> new AssertionError("Constructor has no code"));
        var instructions =
            code.elementStream()
                .flatMap(e -> e instanceof Instruction instruction ? Stream.of(instruction) : null)
                .toList();
        var lastInstruction =
            instructions.get(instructions.size() - 2); // -2 because last is RETURN
        if (!(lastInstruction instanceof InvokeInstruction invokeInstruction)) {
          throw new AssertionError(
              "lastInstruction is neither super() nor this() " + lastInstruction);
        }
        assertAll(
            () -> assertEquals(Opcode.INVOKESPECIAL, invokeInstruction.opcode()),
            () -> assertEquals("<init>", invokeInstruction.name().stringValue())
        );
      }
    }
  }


  @Nested
  public class Q2 {

    @Test
    public void lateFeeNoLateFeeAtExactly15Days() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);

      assertEquals(
          0L,
          library.lateFee(loan, JUNE_16));
    }

    @Test
    public void lateFeeOneLateDayForBook() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);

      assertEquals(
          5L,
          library.lateFee(loan, JUNE_17));
    }

    @Test
    public void lateFeeSeveralLateDaysForBook() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);

      assertEquals(
          25L,
          library.lateFee(loan, JUNE_21));
    }

    @Test
    public void lateFeeOneLateDayForMagazine() {
      var library = new Library();
      var loan = new Loan("Alice", new Magazine("Magazine"), JUNE_1);

      assertEquals(
          2L,
          library.lateFee(loan, JUNE_17));
    }

    @Test
    public void lateFeeSeveralLateDaysForMagazine() {
      var library = new Library();
      var loan = new Loan("Alice", new Magazine("Magazine"), JUNE_1);

      assertEquals(
          10L,
          library.lateFee(loan, JUNE_21));
    }

    @Test
    public void lateFeeOneLateDayForComic() {
      var library = new Library();
      var loan = new Loan("Alice", new Comic("Comic"), JUNE_1);

      assertEquals(
          3L,
          library.lateFee(loan, JUNE_17));
    }

    @Test
    public void lateFeeSeveralLateDaysForComic() {
      var library = new Library();
      var loan = new Loan("Alice", new Comic("Comic"), JUNE_1);

      assertEquals(
          15L,
          library.lateFee(loan, JUNE_21));
    }


    @Test
    public void lateFeeForMemberHasNoLoan() {
      var library = new Library();
      library.addLoan(new Loan("Bob", new Book("Book"), JUNE_1));

      assertEquals(
          0L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberHasOneBookLoan() {
      var library = new Library();
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      assertEquals(
          20L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberOnlyPaysForTheirLoans() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Magazine("Magazine"), JUNE_1));

      assertEquals(
          20L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberPaysSumOfAllTheirLoans() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));

      assertEquals(
          40L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberPaysSumOfAllTheirLoansDifferentDates() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_20));
      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic2"), JUNE_2));

      assertEquals(
          52L,
          library.lateFeeForMember("Alice", JUNE_21));
    }

    @Test
    public void lateFeeForMemberNullMember() {
      var library = new Library();

      assertThrows(
          NullPointerException.class,
          () -> library.lateFeeForMember(null, JUNE_20));
    }

    @Test
    public void lateFeeForMemberNullDate() {
      var library = new Library();

      assertThrows(
          NullPointerException.class,
          () -> library.lateFeeForMember("Alice", null));
    }

    @Test
    public void lateFeeForMemberUsesAllLoans() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), JUNE_1));

      assertEquals(
          40L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberIgnoresOnTimeLoans() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_16));

      assertEquals(
          20L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void lateFeeForMemberWithSeveralMembers() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Comic("Comic"), JUNE_1));

      assertEquals(
          40L,
          library.lateFeeForMember("Alice", JUNE_20));

      assertEquals(
          32L,
          library.lateFeeForMember("Bob", JUNE_20));
    }
  }


  @Nested
  public class Q3 {

    @Test
    public void lateFeeForMemberUsesStream() throws IOException {
      byte[] bytes;
      try(var input = Library.class.getResourceAsStream("Library.class")) {
        if (input == null) {
          throw new AssertionError("Library.class not found");
        }
        bytes = input.readAllBytes();
      }
      var classModel = ClassFile.of().parse(bytes);

      var method = classModel.methods().stream()
          .filter(m -> m.methodName().equalsString("lateFeeForMember"))
          .filter(m -> m.methodType().stringValue()
              .startsWith("(Ljava/lang/String;Ljava/time/LocalDate;)"))
          .findFirst()
          .orElseThrow();

      var usesStream = false;
      var code = method.code().orElseThrow();
      for(var element : code.elementList()) {
        if (element instanceof InvokeInstruction invoke) {
          var owner = invoke.owner();
          if (owner.asInternalName().equals("java/util/stream/Stream")) {
            usesStream = true;
          }
        }
      }

      assertTrue(usesStream, "lateFeeForMember must use a stream");
    }
  }


  @Nested
  public class Q4 {

    @Test
    public void findLoansByMember() {
      var library = new Library();

      var aliceLoan = new Loan("Alice", new Book("Book"), JUNE_1);
      var bobLoan = new Loan("Bob", new Magazine("Magazine"), JUNE_1);

      library.addLoan(aliceLoan);
      library.addLoan(bobLoan);

      var result = library.findLoans(aliceLoan::equals);

      assertEquals(List.of(aliceLoan), result);
    }

    @Test
    public void findLoansWithNoMatch() {
      var library = new Library();
      var aliceLoan = new Loan("Alice", new Book("Book"), JUNE_1);
      var bobLoan = new Loan("Bob", new Book("Book"), JUNE_20);
      library.addLoan(aliceLoan);

      var result = library.findLoans(bobLoan::equals);

      assertEquals(List.of(), result);
    }

    @Test
    public void findLoansNull() {
      var library = new Library();

      assertThrows(NullPointerException.class, () -> library.findLoans(null));
    }

    @Test
    public void findLoansResultIsNotMutable() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);
      library.addLoan(loan);

      var result = library.findLoans(loan::equals);

      assertThrows(UnsupportedOperationException.class, () -> result.add(new Loan("Bob", new Book("Book"), JUNE_16)));
    }
  }


  @Nested
  public class Q5 {

    @Test
    public void findAllLoansWithOneCriterion() {
      var library = new Library();
      var aliceLoan = new Loan("Alice", new Book("Book"), JUNE_1);
      var bobLoan = new Loan("Bob", new Book("Book"), JUNE_1);
      library.addLoan(aliceLoan);
      library.addLoan(bobLoan);

      var loans = library.findAllLoans(List.of(aliceLoan::equals));

      assertEquals(List.of(aliceLoan), loans);
    }

    @Test
    public void findAllLoansWithSeveralDifferentCriteria() {
      var library = new Library();
      var aliceLoan = new Loan("Alice", new Book("Book"), JUNE_1);
      var bobLoan = new Loan("Bob", new Book("Book"), JUNE_2);
      var ianLoan = new Loan("Ian", new Book("Book"), JUNE_21);
      library.addLoan(aliceLoan);
      library.addLoan(bobLoan);
      library.addLoan(ianLoan);

      var loans = library.findAllLoans(List.of(aliceLoan::equals, aliceLoan::equals));

      assertEquals(List.of(aliceLoan), loans);
    }

    @Test
    public void findAllLoansRequiresAllCriteria() {
      var library = new Library();
      var aliceBook = new Loan("Alice", new Book("Book"), JUNE_1);
      var aliceMagazine = new Loan("Alice", new Magazine("Magazine"), JUNE_1);
      var bobBook = new Loan("Bob", new Book("Book"), JUNE_1);
      library.addLoan(aliceBook);
      library.addLoan(aliceMagazine);
      library.addLoan(bobBook);

      var loans = library.findAllLoans(List.of(aliceBook::equals, aliceMagazine::equals));

      assertEquals(List.of(), loans);
    }

    @Test
    public void findAllLoansEmptyCriteria() {
      var library = new Library();
      var loan1 = new Loan("Alice", new Book("Book"), JUNE_1);
      var loan2 = new Loan("Bob", new Magazine("Magazine"), JUNE_1);
      library.addLoan(loan1);
      library.addLoan(loan2);

      var loans = library.findAllLoans(List.of());

      assertEquals(
          List.of(loan1, loan2),
          loans);
    }

    @Test
    public void findAllLoansNullList() {
      var library = new Library();

      assertThrows(NullPointerException.class,
          () -> library.findAllLoans(null));
    }

    @Test
    public void findAllLoansNullCriteria() {
      var library = new Library();
      var loan = new Loan("Alice", new Book("Book"), JUNE_1);

      assertThrows(NullPointerException.class,
          () -> library.findAllLoans(Arrays.asList(loan::equals, null)));
    }
  }


  @Nested
  public class Q6 {

    @Test
    public void countByDocumentEmpty() {
      var library = new Library();

      assertEquals(Map.of(), library.countByDocument());
    }

    @Test
    public void countByDocument() {
      var library = new Library();
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));
      library.addLoan(new Loan("Bob", new Comic("Comic"), JUNE_1));
      library.addLoan(new Loan("Charlie", new Comic("Comic"), JUNE_1));

      var result = library.countByDocument();

      assertEquals(
          Map.of(
              DocumentKind.BOOK, 2L,
              DocumentKind.MAGAZINE, 1L,
              DocumentKind.COMIC, 3L),
          result);
    }

    @Test
    public void countByDocumentContainsOnlyExistingKinds() {
      var library = new Library();
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), JUNE_1));

      var result = library.countByDocument();

      assertEquals(
          Map.of(DocumentKind.BOOK, 2L),
          result);
    }

    @Test
    public void borrowDaysByDocumentContainsOnlyExistingKinds() {
      var library = new Library();
      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), LocalDate.of(2026, 6, 10)));

      var result = library.borrowDaysByDocument(JUNE_20);

      assertEquals(
          Map.of(DocumentKind.BOOK, 29L),
          result);
    }

    @Test
    public void borrowDaysByDocument() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Bob", new Book("Book"), LocalDate.of(2026, 6, 10)));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic"), LocalDate.of(2026, 6, 15)));

      var result = library.borrowDaysByDocument(JUNE_20);

      assertEquals(
          Map.of(
              DocumentKind.BOOK, 29L,
              DocumentKind.MAGAZINE, 19L,
              DocumentKind.COMIC, 5L),
          result);
    }

    @Test
    public void borrowDaysByDocumentEmpty() {
      var library = new Library();

      assertEquals(Map.of(), library.borrowDaysByDocument(JUNE_20));
    }

    @Test
    public void borrowDaysByDocumentNullDate() {
      var library = new Library();

      assertThrows(NullPointerException.class,
          () -> library.borrowDaysByDocument(null));
    }
  }


  @Nested
  public class Q7 {

    private static Set<String> invokedMethods(ClassModel classModel, String methodName) {
      var method = classModel.methods().stream()
          .filter(m -> m.methodName().equalsString(methodName))
          .findFirst()
          .orElseThrow(() -> new AssertionError("Method not found: " + methodName));

      var code = method.code().orElseThrow();

      var invoked = new HashSet<String>();
      code.forEach(element -> {
        if (element instanceof InvokeInstruction invoke) {
          var owner = invoke.owner().asInternalName();
          if (owner.endsWith("Library")) {
            invoked.add( invoke.name().stringValue() + invoke.type().stringValue());
          }
        }
      });
      return invoked;
    }

    @Test
    public void countAndBorrowDaysShareTheSameUnderlyingAlgorithm() throws IOException {
      var bytes = Library.class.getResourceAsStream("Library.class");
      assertNotNull(bytes);
      var classModel = ClassFile.of().parse(bytes.readAllBytes());
      var countMethods = invokedMethods(classModel, "countByDocument");
      var borrowDaysMethods = invokedMethods(classModel, "borrowDaysByDocument");

      assertFalse(Collections.disjoint(countMethods, borrowDaysMethods));
    }
  }


  @Nested
  public class Q8 {

    @Test
    public void customBookFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertEquals(
          40L,
          library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void customMagazineFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertEquals(
          80L,
          library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void customComicFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertEquals(
          120L,
          library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void customFeesAreUsedForEachDocumentKind() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));
      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));
      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      // 4 * 1 + 4 * 2 + 4 * 3 = 24
      assertEquals(
          240L,
          library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void customFeesDoNotChangeDefaultFees() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 100L,
          DocumentKind.MAGAZINE, 200L,
          DocumentKind.COMIC, 300L);

      assertEquals(
          400L,
          library.lateFeeForMember("Alice", JUNE_20, fees));

      assertEquals(
          20L,
          library.lateFeeForMember("Alice", JUNE_20));
    }

    @Test
    public void customFeesOnlyApplyToLateDays() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 100L,
          DocumentKind.MAGAZINE, 200L,
          DocumentKind.COMIC, 300L);

      assertEquals(
          0L,
          library.lateFeeForMember("Alice", JUNE_16, fees));
    }

    @Test
    public void nullMemberWithCustomFees() {
      var library = new Library();

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertThrows(NullPointerException.class,
          () -> library.lateFeeForMember(null, JUNE_20, fees));
    }

    @Test
    public void nullDateWithCustomFees() {
      var library = new Library();

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertThrows(NullPointerException.class,
          () -> library.lateFeeForMember("Alice", null, fees));
    }

    @Test
    public void nullFees() {
      var library = new Library();

      assertThrows(NullPointerException.class,
          () -> library.lateFeeForMember("Alice", JUNE_20, null));
    }

    @Test
    public void missingBookFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Book("Book"), JUNE_1));

      var fees = Map.of(
          DocumentKind.MAGAZINE, 20L,
          DocumentKind.COMIC, 30L);

      assertThrows(IllegalArgumentException.class,
          () -> library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void missingMagazineFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Magazine("Magazine"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.COMIC, 30L);

      assertThrows(IllegalArgumentException.class,
          () -> library.lateFeeForMember("Alice", JUNE_20, fees));
    }

    @Test
    public void missingComicFee() {
      var library = new Library();

      library.addLoan(new Loan("Alice", new Comic("Comic"), JUNE_1));

      var fees = Map.of(
          DocumentKind.BOOK, 10L,
          DocumentKind.MAGAZINE, 20L);

      assertThrows(IllegalArgumentException.class,
          () -> library.lateFeeForMember("Alice", JUNE_20, fees));
    }
  }
}
