package fr.uge.bankstat;

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

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

import java.lang.ref.WeakReference;
import java.lang.reflect.AccessFlag;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public final class BankStatTest {

  @Nested
  public class Q1 {

    @Test
    public void testWithdrawCreation() {
      var withdraw = new BankStat.Withdraw("W123");
      assertEquals("W123", withdraw.withdrawId());
    }

    @Test
    public void testWithdrawNullId() {
      assertThrows(NullPointerException.class, () -> new BankStat.Withdraw(null));
    }

    @Test
    public void testDepositCreation() {
      var deposit = new BankStat.Deposit("D456");
      assertEquals("D456", deposit.depositId());
    }

    @Test
    public void testDepositNullId() {
      assertThrows(NullPointerException.class, () -> new BankStat.Deposit(null));
    }

    @Test
    public void testWithdrawEquals() {
      var withdraw1 = new BankStat.Withdraw("W123");
      var withdraw2 = new BankStat.Withdraw("W123");
      var withdraw3 = new BankStat.Withdraw("W456");

      assertEquals(withdraw1, withdraw2);
      assertNotEquals(withdraw1, withdraw3);
    }

    @Test
    public void testDepositEquals() {
      var deposit1 = new BankStat.Deposit("D123");
      var deposit2 = new BankStat.Deposit("D123");
      var deposit3 = new BankStat.Deposit("D456");

      assertEquals(deposit1, deposit2);
      assertNotEquals(deposit1, deposit3);
    }

    @Test
    public void testWithdrawHashCode() {
      var withdraw1 = new BankStat.Withdraw("W123");
      var withdraw2 = new BankStat.Withdraw("W123");

      assertEquals(withdraw1.hashCode(), withdraw2.hashCode());
    }

    @Test
    public void testDepositHashCode() {
      var deposit1 = new BankStat.Deposit("D123");
      var deposit2 = new BankStat.Deposit("D123");

      assertEquals(deposit1.hashCode(), deposit2.hashCode());
    }

    @Test
    public void testWithdrawToString() {
      var withdraw = new BankStat.Withdraw("W123");
      var withdrawString = withdraw.toString();
      assertTrue(withdrawString.contains("W123"));
    }

    @Test
    public void testDepositToString() {
      var deposit = new BankStat.Deposit("D123");
      var depositString = deposit.toString();
      assertTrue(depositString.contains("D123"));
    }

    @Test
    public void testDifferentTransactionTypesNotEqual() {
      var withdraw = new BankStat.Withdraw("T123");
      var deposit = new BankStat.Deposit("T123");

      assertNotEquals(withdraw, deposit);
    }

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

    @Test
    public void testDepositIsImmutable() {
      assertTrue(BankStat.Deposit.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(BankStat.Deposit.class.accessFlags().contains(AccessFlag.FINAL));

      assertTrue(Arrays.stream(BankStat.Deposit.class.getDeclaredFields())
          .allMatch(f -> f.accessFlags().containsAll(Set.of(AccessFlag.PRIVATE, AccessFlag.FINAL))));
    }

    @Test
    public void testWithdrawIsImmutable() {
      assertTrue(BankStat.Withdraw.class.accessFlags().contains(AccessFlag.PUBLIC));
      assertTrue(BankStat.Withdraw.class.accessFlags().contains(AccessFlag.FINAL));

      assertTrue(Arrays.stream(BankStat.Withdraw.class.getDeclaredFields())
          .allMatch(f -> f.accessFlags().containsAll(Set.of(AccessFlag.PRIVATE, AccessFlag.FINAL))));
    }

    @Test
    public void testAllMemberClassesAreFinal() {
      assertTrue(Arrays.stream(BankStat.class.getDeclaredClasses())
          .filter(c -> !c.accessFlags().contains(AccessFlag.ABSTRACT))
          .allMatch(c -> c.accessFlags().contains(AccessFlag.FINAL)));
    }
  }


  @Nested
  public class Q2 {

    @Test
    public void testConstructor() {
      var bankStat = new BankStat();
      assertEquals(0, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testAddSingleWithdraw() {
      var bankStat = new BankStat();
      var withdraw = new BankStat.Withdraw("W123");

      bankStat.addTransaction(withdraw);

      assertEquals(1, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testAddSingleDeposit() {
      var bankStat = new BankStat();
      var deposit = new BankStat.Deposit("D123");

      bankStat.addTransaction(deposit);

      assertEquals(0, bankStat.withdrawCount());
      assertEquals(1, bankStat.depositCount());
    }

    @Test
    public void testAddMultipleWithdraws() {
      var bankStat = new BankStat();
      var withdraw1 = new BankStat.Withdraw("W123");
      var withdraw2 = new BankStat.Withdraw("W456");
      var withdraw3 = new BankStat.Withdraw("W789");

      bankStat.addTransaction(withdraw1);
      bankStat.addTransaction(withdraw2);
      bankStat.addTransaction(withdraw3);

      assertEquals(3, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testAddMultipleDeposits() {
      var bankStat = new BankStat();
      var deposit1 = new BankStat.Deposit("D123");
      var deposit2 = new BankStat.Deposit("D456");
      var deposit3 = new BankStat.Deposit("D789");

      bankStat.addTransaction(deposit1);
      bankStat.addTransaction(deposit2);
      bankStat.addTransaction(deposit3);

      assertEquals(0, bankStat.withdrawCount());
      assertEquals(3, bankStat.depositCount());
    }

    @Test
    public void testAddMixedTransactions() {
      var bankStat = new BankStat();
      var withdraw1 = new BankStat.Withdraw("W123");
      var deposit1 = new BankStat.Deposit("D456");
      var withdraw2 = new BankStat.Withdraw("W789");
      var deposit2 = new BankStat.Deposit("D101");
      var deposit3 = new BankStat.Deposit("D112");

      bankStat.addTransaction(withdraw1);
      bankStat.addTransaction(deposit1);
      bankStat.addTransaction(withdraw2);
      bankStat.addTransaction(deposit2);
      bankStat.addTransaction(deposit3);

      assertEquals(2, bankStat.withdrawCount());
      assertEquals(3, bankStat.depositCount());
    }

    @Test
    public void testAddTransactionNullParameter() {
      var bankStat = new BankStat();
      assertThrows(NullPointerException.class, () -> bankStat.addTransaction(null));
    }

    @Test
    public void testCountsAfterSeveralTransactions() {
      var bankStat = new BankStat();

      for (int i = 0; i < 8; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        bankStat.addTransaction(new BankStat.Deposit("D" + i));
      }

      assertEquals(8, bankStat.withdrawCount());
      assertEquals(8, bankStat.depositCount());
    }

    @Test
    public void testCountsAreIndependent() {
      var bankStat1 = new BankStat();
      var bankStat2 = new BankStat();

      bankStat1.addTransaction(new BankStat.Withdraw("W1"));
      bankStat1.addTransaction(new BankStat.Withdraw("W2"));

      bankStat2.addTransaction(new BankStat.Deposit("D1"));

      assertEquals(2, bankStat1.withdrawCount());
      assertEquals(0, bankStat1.depositCount());

      assertEquals(0, bankStat2.withdrawCount());
      assertEquals(1, bankStat2.depositCount());
    }

    @Test
    public void testSameIdDifferentTypes() {
      var bankStat = new BankStat();
      var withdraw = new BankStat.Withdraw("T123");
      var deposit = new BankStat.Deposit("T123");

      bankStat.addTransaction(withdraw);
      bankStat.addTransaction(deposit);

      assertEquals(1, bankStat.withdrawCount());
      assertEquals(1, bankStat.depositCount());
    }

    @Test
    public void testDepositsAreGarbageCollectable() {
      var bankStat = new BankStat();
      var deposit = new BankStat.Deposit("D123");
      bankStat.addTransaction(deposit);

      var ref = new WeakReference<>(deposit);
      deposit = null;
      System.gc();

      assertTrue(ref.refersTo(null));
    }

    @Test
    public void testWithdrawsAreGarbageCollectable() {
      var bankStat = new BankStat();
      var withdraw = new BankStat.Withdraw("W123");
      bankStat.addTransaction(withdraw);

      var ref = new WeakReference<>(withdraw);
      withdraw = null;
      System.gc();

      assertTrue(ref.refersTo(null));
    }

    @Test
    public void testFieldsDoNotUseCollection() {
      assertEquals(0,
          Arrays.stream(BankStat.class.getDeclaredFields())
              .filter(f -> f.getType().getPackageName().startsWith("java.util"))
              .count());
    }

    @Test
    public void testOnlyTwoIntFieldsExist() {
      assertEquals(2,
          Arrays.stream(BankStat.class.getDeclaredFields())
              .filter(f -> f.getType() == int.class)
              .count());
    }

    @Test
    public void testAllFieldsArePrivate() {
      assertTrue(
          Arrays.stream(BankStat.class.getDeclaredFields())
              .allMatch(f -> f.accessFlags().contains(AccessFlag.PRIVATE)));
    }
  }


  @Nested
  public class Q3 {

    @Test
    public void testArrayGrowthAt16Elements() {
      var bankStat = new BankStat();
      for (var i = 0; i < 16; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + i));
      }

      assertEquals(16, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());

      // Trigger a resize
      bankStat.addTransaction(new BankStat.Deposit("D17"));

      assertEquals(16, bankStat.withdrawCount());
      assertEquals(1, bankStat.depositCount());
    }

    @Test
    public void testArrayGrowthPreservesElements() {
      var bankStat = new BankStat();
      for (var i = 0; i < 30; i++) {
        if (i % 3 == 0) {
          var id = "W" + i;
          bankStat.addTransaction(new BankStat.Withdraw(id));
        } else {
          var id = "D" + i;
          bankStat.addTransaction(new BankStat.Deposit(id));
        }
      }

      assertEquals(10, bankStat.withdrawCount());
      assertEquals(20, bankStat.depositCount());
    }

    @Test
    public void testArrayGrowthWithMixedTransactions() {
      var bankStat = new BankStat();
      for (var i = 0; i < 20; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("D" + i));
        }
      }

      assertEquals(10, bankStat.withdrawCount());
      assertEquals(10, bankStat.depositCount());
    }

    @Test
    public void testMultipleArrayGrowths() {
      var bankStat = new BankStat();
      var totalTransactions = 1_000_000;

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for (var i = 0; i < totalTransactions; i++) {
          if (i < totalTransactions / 2) {
            bankStat.addTransaction(new BankStat.Withdraw("W" + i));
          } else {
            bankStat.addTransaction(new BankStat.Deposit("D" + i));
          }
        }
      });

      assertEquals(totalTransactions / 2, bankStat.withdrawCount());
      assertEquals(totalTransactions / 2, bankStat.depositCount());
    }

    @Test
    public void testArrayGrowthWithDuplicateIds() {
      var bankStat = new BankStat();
      for (var i = 0; i < 25; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("DUPLICATE"));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("UNIQUE" + i));
        }
      }

      assertEquals(13, bankStat.withdrawCount());
      assertEquals(12, bankStat.depositCount());
    }

    @Test
    public void testLargeNumberOfTransactions() {
      var bankStat = new BankStat();
      var largeNumber = 1_000_000;

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for (var i = 0; i < largeNumber; i++) {
          bankStat.addTransaction(new BankStat.Withdraw("LARGE" + i));
        }
      });

      assertEquals(largeNumber, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testArrayGrowthExactlyAt16Then32() {
      var bankStat = new BankStat();
      for (var i = 0; i < 16; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("FIRST" + i));
      }

      // trigger a resize
      for (var i = 0; i < 16; i++) {
        bankStat.addTransaction(new BankStat.Deposit("SECOND" + i));
      }

      assertEquals(16, bankStat.withdrawCount());
      assertEquals(16, bankStat.depositCount());
    }

    @Test
    public void testCountsByIdWorksAfterArrayGrowth() {
      var bankStat = new BankStat();
      for (var i = 0; i < 25; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + (i % 5))); // Creates W0, W1, W2, W3, W4 repeating
      }

      assertEquals(25, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testArrayGrowthWithEmptyIds() {
      var bankStat = new BankStat();

      for (var i = 0; i < 20; i++) {
        if (i == 10) {
          bankStat.addTransaction(new BankStat.Withdraw("")); // Empty string
        } else {
          bankStat.addTransaction(new BankStat.Deposit("ID" + i));
        }
      }

      assertEquals(1, bankStat.withdrawCount());
      assertEquals(19, bankStat.depositCount());
    }
  }




  @Nested
  public class Q4 {

    @Test
    public void testContainsIdEmptyBankStat() {
      var bankStat = new BankStat();
      assertFalse(bankStat.containsId("W123"));
      assertFalse(bankStat.containsId("D456"));
    }

    @Test
    public void testContainsIdWithSingleWithdraw() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertTrue(bankStat.containsId("W123"));
      assertFalse(bankStat.containsId("W456"));
    }

    @Test
    public void testContainsIdWithSingleDeposit() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Deposit("D123"));

      assertTrue(bankStat.containsId("D123"));
      assertFalse(bankStat.containsId("D456"));
    }

    @Test
    public void testContainsIdWithMultipleTransactions() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));
      bankStat.addTransaction(new BankStat.Withdraw("W789"));

      assertTrue(bankStat.containsId("W123"));
      assertTrue(bankStat.containsId("D456"));
      assertTrue(bankStat.containsId("W789"));
      assertFalse(bankStat.containsId("W999"));
      assertFalse(bankStat.containsId("D999"));
    }

    @Test
    public void testContainsIdWithDuplicateIds() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("W123"));
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertTrue(bankStat.containsId("W123"));
      assertFalse(bankStat.containsId("W456"));
    }

    @Test
    public void testContainsIdSameIdDifferentTransactionTypes() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("T123"));
      bankStat.addTransaction(new BankStat.Deposit("T123"));

      assertTrue(bankStat.containsId("T123"));
      assertFalse(bankStat.containsId("T456"));
    }

    @Test
    public void testContainsIdCaseSensitive() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertTrue(bankStat.containsId("W123"));
      assertFalse(bankStat.containsId("w123"));
      assertFalse(bankStat.containsId("W123 "));
      assertFalse(bankStat.containsId(" W123"));
    }

    @Test
    public void testContainsIdWithNullParameter() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertThrows(NullPointerException.class, () -> bankStat.containsId(null));
    }

    @Test
    public void testContainsIdWithEmptyString() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw(""));
      bankStat.addTransaction(new BankStat.Deposit("D123"));

      assertTrue(bankStat.containsId(""));
      assertTrue(bankStat.containsId("D123"));
      assertFalse(bankStat.containsId("W123"));
    }

    @Test
    public void testContainsIdAfterManyTransactions() {
      var bankStat = new BankStat();

      for (var i = 0; i < 1_000_000; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        bankStat.addTransaction(new BankStat.Deposit("D" + i));
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        assertTrue(bankStat.containsId("W0"));
        assertTrue(bankStat.containsId("W50"));
        assertTrue(bankStat.containsId("W9999"));
        assertTrue(bankStat.containsId("D0"));
        assertTrue(bankStat.containsId("D50"));
        assertTrue(bankStat.containsId("D987654"));

        assertFalse(bankStat.containsId("W1000000"));
        assertFalse(bankStat.containsId("D1000000"));
      });
    }

    @Test
    public void testContainsIdFirstAndLastElements() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("FIRST"));
      bankStat.addTransaction(new BankStat.Deposit("MIDDLE"));
      bankStat.addTransaction(new BankStat.Withdraw("LAST"));

      assertTrue(bankStat.containsId("FIRST"));
      assertTrue(bankStat.containsId("MIDDLE"));
      assertTrue(bankStat.containsId("LAST"));
    }

    @Test
    public void testContainsIdDoesNotModifyState() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      assertTrue(bankStat.containsId("W123"));
      assertTrue(bankStat.containsId("D456"));
      assertFalse(bankStat.containsId("NOTFOUND"));
    }

    @Test
    public void testContainsIdWithSpecialCharacters() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W-123"));
      bankStat.addTransaction(new BankStat.Deposit("D_456"));
      bankStat.addTransaction(new BankStat.Withdraw("W.789"));
      bankStat.addTransaction(new BankStat.Deposit("D@ABC"));

      assertTrue(bankStat.containsId("W-123"));
      assertTrue(bankStat.containsId("D_456"));
      assertTrue(bankStat.containsId("W.789"));
      assertTrue(bankStat.containsId("D@ABC"));
      assertFalse(bankStat.containsId("W123"));
      assertFalse(bankStat.containsId("D456"));
    }

    @Test
    public void testContainsIdMultipleCalls() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertTrue(bankStat.containsId("W123"));
      assertTrue(bankStat.containsId("W123"));
      assertTrue(bankStat.containsId("W123"));

      assertFalse(bankStat.containsId("W456"));
      assertFalse(bankStat.containsId("W456"));
      assertFalse(bankStat.containsId("W456"));
    }

    @Test
    public void testArrayGrowthAt16Elements() {
      var bankStat = new BankStat();
      for (var i = 0; i < 16; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + i));
      }

      // Trigger a resize
      bankStat.addTransaction(new BankStat.Deposit("D17"));

      for (var i = 0; i < 16; i++) {
        assertTrue(bankStat.containsId("W" + i));
      }
      assertTrue(bankStat.containsId("D17"));
    }

    @Test
    public void testArrayGrowthWithMixedTransactions() {
      var bankStat = new BankStat();
      for (var i = 0; i < 20; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("D" + i));
        }
      }

      for (var i = 0; i < 20; i++) {
        if (i % 2 == 0) {
          assertTrue(bankStat.containsId("W" + i));
        } else {
          assertTrue(bankStat.containsId("D" + i));
        }
      }
    }

    @Test
    public void testArrayGrowthWithDuplicateIds() {
      var bankStat = new BankStat();
      for (var i = 0; i < 25; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("DUPLICATE"));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("UNIQUE" + i));
        }
      }

      // Verify duplicate ID is still found
      assertTrue(bankStat.containsId("DUPLICATE"));

      // Verify unique IDs are also accessible
      for (var i = 1; i < 25; i += 2) {
        assertTrue(bankStat.containsId("UNIQUE" + i));
      }
    }

    @Test
    public void testLargeNumberOfTransactions() {
      var bankStat = new BankStat();
      var largeNumber = 1_000_000;
      for (var i = 0; i < largeNumber; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("LARGE" + i));
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        assertTrue(bankStat.containsId("LARGE0"));
        assertTrue(bankStat.containsId("LARGE" + (largeNumber / 2)));
        assertTrue(bankStat.containsId("LARGE" + (largeNumber - 1)));

        assertFalse(bankStat.containsId("LARGE" + largeNumber));
      });
    }

    @Test
    public void testArrayGrowthWithEmptyIds() {
      var bankStat = new BankStat();
      for (var i = 0; i < 20; i++) {
        if (i == 10) {
          bankStat.addTransaction(new BankStat.Withdraw("")); // Empty string
        } else {
          bankStat.addTransaction(new BankStat.Deposit("ID" + i));
        }
      }

      assertTrue(bankStat.containsId(""));

      for (var i = 0; i < 20; i++) {
        if (i != 10) {
          assertTrue(bankStat.containsId("ID" + i));
        }
      }
    }

    @Test
    public void testArrayGrowthPreservesContainsIdFunctionality() {
      var bankStat = new BankStat();
      for (var i = 0; i < 50; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("TEST" + i));
      }

      for (var i = 0; i < 50; i++) {
        assertTrue(bankStat.containsId("TEST" + i));
      }

      assertFalse(bankStat.containsId("TEST50"));
      assertFalse(bankStat.containsId("NOTFOUND"));
      assertFalse(bankStat.containsId("test0")); // Case sensitive
    }
  }


  @Nested
  public class Q5 {

    @Test
    public void testReplaceIdOnEmptyBankStat() {
      var bankStat = new BankStat();
      bankStat.replaceId("any"::equalsIgnoreCase, String::strip);

      assertEquals(0, bankStat.withdrawCount());
      assertEquals(0, bankStat.depositCount());
    }

    @Test
    public void testReplaceIdWithMatchingCondition() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W001"));
      bankStat.addTransaction(new BankStat.Deposit("D001"));
      bankStat.addTransaction(new BankStat.Withdraw("W002"));

      bankStat.replaceId("W001"::equals, Map.of("W001", "T007")::get);

      assertEquals(2, bankStat.withdrawCount());
      assertEquals(1, bankStat.depositCount());
      assertTrue(bankStat.containsId("T007"));
      assertFalse(bankStat.containsId("W001"));
    }

    @Test
    public void testReplaceIdWithNonMatchingCondition() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("  W001"));
      bankStat.addTransaction(new BankStat.Deposit("  D001"));

      bankStat.replaceId(String::isEmpty, String::stripIndent);

      assertTrue(bankStat.containsId("  W001"));
      assertTrue(bankStat.containsId("  D001"));
      assertFalse(bankStat.containsId("W001"));
      assertFalse(bankStat.containsId("D001"));
    }

    @Test
    public void testReplaceIdWithMultipleMatches() {
      // Add transactions with similar IDs
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W001"));
      bankStat.addTransaction(new BankStat.Deposit("W002"));
      bankStat.addTransaction(new BankStat.Withdraw("W001"));
      bankStat.addTransaction(new BankStat.Deposit("W003"));

      bankStat.replaceId("W001"::equals, String::toLowerCase);

      assertTrue(bankStat.containsId("w001"));
      assertTrue(bankStat.containsId("W002"));
      assertTrue(bankStat.containsId("W003"));
      assertFalse(bankStat.containsId("W001"));
    }

    @Test
    public void testReplaceIdAfterArrayResize() {
      var bankStat = new BankStat();

      for (int i = 1; i <= 20; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("D" + i));
        }
      }

      bankStat.replaceId("W12"::equals, String::toLowerCase);

      assertTrue(bankStat.containsId("w12"));
      assertTrue(bankStat.containsId("W10"));
      assertTrue(bankStat.containsId("D11"));
      assertFalse(bankStat.containsId("W12"));
      assertFalse(bankStat.containsId("D10"));
      assertFalse(bankStat.containsId("W11"));

      assertEquals(10, bankStat.withdrawCount());
      assertEquals(10, bankStat.depositCount());
    }

    @Test
    public void testReplaceIdDoNotInsertNull() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W1"));
      bankStat.addTransaction(new BankStat.Deposit("D1"));

      var replacementMap = new HashMap<Object, String>();
      assertThrows(NullPointerException.class,
          () -> bankStat.replaceId("D1"::equals, replacementMap::get));
    }

    @Test
    public void testReplaceIdWithNullCondition() {
      var bankStat = new BankStat();

      assertThrows(NullPointerException.class, () -> {
        bankStat.replaceId(null, String::strip);
      });
    }

    @Test
    public void testReplaceIdWithNullReplacement() {
      var bankStat = new BankStat();

      assertThrows(NullPointerException.class, () -> {
        bankStat.replaceId(String::isEmpty, null);
      });
    }
  }


  @Nested
  public class Q6 {

    @Test
    public void testCountByIdEmptyBankStat() {
      BankStat bankStat = new BankStat();
      Map<String, Integer> result = bankStat.countById();
      assertTrue(result.isEmpty());
    }

    @Test
    public void testCountByIdSingleTransaction() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      var result = bankStat.countById();
      var expected = Map.of("W123", 1);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdMultipleUniqueTransactions() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));
      bankStat.addTransaction(new BankStat.Withdraw("W789"));

      var result = bankStat.countById();
      var expected = Map.of("W123", 1, "D456", 1, "W789", 1);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdDuplicateIds() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("W123"));
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      var result = bankStat.countById();
      var expected = Map.of("W123", 3);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdMixedDuplicatesAndUniques() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D789"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      var result = bankStat.countById();
      var expected = Map.of("W123", 2, "D456", 2, "D789", 1);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdSameIdDifferentTransactionTypes() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("T123"));
      bankStat.addTransaction(new BankStat.Deposit("T123"));
      bankStat.addTransaction(new BankStat.Withdraw("T456"));

      var result = bankStat.countById();
      var expected = Map.of("T123", 2, "T456", 1);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdLargeNumberOfTransactions() {
      var bankStat = new BankStat();

      for (var i = 0; i < 16; i++) {
        var id = "ID" + (i % 5);
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw(id));
        } else {
          bankStat.addTransaction(new BankStat.Deposit(id));
        }
      }

      var result = bankStat.countById();
      var expected =
          Map.of("ID0", 4, "ID1", 3, "ID2", 3, "ID3", 3, "ID4", 3);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdAfterMultipleCalls() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      var result1 = bankStat.countById();
      var expected1 = Map.of("W123", 1);
      assertEquals(expected1, result1);

      bankStat.addTransaction(new BankStat.Deposit("W123"));

      var result2 = bankStat.countById();
      var expected2 = Map.of("W123", 2);
      assertEquals(expected2, result2);
    }

    @Test
    public void testCountByIdWithEmptyStringId() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw(""));
      bankStat.addTransaction(new BankStat.Deposit(""));
      bankStat.addTransaction(new BankStat.Withdraw("NORMAL"));

      var result = bankStat.countById();
      var expected = Map.of("", 2, "NORMAL", 1);
      assertEquals(expected, result);
    }

    @Test
    public void testCountByIdDoesNotAffectOriginalState() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      var initialWithdrawCount = bankStat.withdrawCount();
      var initialDepositCount = bankStat.depositCount();

      bankStat.countById();

      assertEquals(initialWithdrawCount, bankStat.withdrawCount());
      assertEquals(initialDepositCount, bankStat.depositCount());
    }


    @Test
    public void testArrayGrowthPreservesElements() {
      var bankStat = new BankStat();
      var expectedIds = new ArrayList<String>();

      for (var i = 0; i < 30; i++) {
        if (i % 3 == 0) {
          var id = "W" + i;
          bankStat.addTransaction(new BankStat.Withdraw(id));
          expectedIds.add(id);
        } else {
          var id = "D" + i;
          bankStat.addTransaction(new BankStat.Deposit(id));
          expectedIds.add(id);
        }
      }

      var countMap = bankStat.countById();
      for (var id : expectedIds) {
        assertTrue(countMap.containsKey(id));
        assertEquals(1, countMap.get(id));
        assertEquals(1, countMap.getOrDefault(id, -1));
      }
    }

    @Test
    public void testArrayGrowthWithDuplicateIds() {
      var bankStat = new BankStat();
      for (var i = 0; i < 25; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("DUPLICATE"));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("UNIQUE" + i));
        }
      }

      var countMap = bankStat.countById();
      assertEquals(13, countMap.get("DUPLICATE"));

      for (var i = 1; i < 25; i += 2) {
        assertEquals(1, countMap.get("UNIQUE" + i));
      }
    }

    @Test
    public void testCountsByIdWorksAfterArrayGrowth() {
      var bankStat = new BankStat();
      for (var i = 0; i < 1_000_000; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("W" + (i % 10)));
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        var countMap = bankStat.countById();

        assertEquals(10, countMap.size());
        for (var i = 0; i < 10; i++) {
          assertEquals(100_000, countMap.get("W" + i));
        }
      });
    }
  }


  @Nested
  public class Q7 {

    @Test
    public void testValidIdCountEmptyBankStat() {
      BankStat bankStat = new BankStat();
      int validTransactionCount = bankStat.validTransactionCount();
      assertEquals(0, validTransactionCount);
    }

    @Test
    public void testValidTransactionCountSingleTransaction() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountMultipleUniqueTransactions() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));
      bankStat.addTransaction(new BankStat.Withdraw("W789"));

      assertEquals(3, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountWithDuplicateIds() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("W123"));
      bankStat.addTransaction(new BankStat.Withdraw("W456"));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountAllDuplicateIds() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("W123"));
      bankStat.addTransaction(new BankStat.Withdraw("D456"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      assertEquals(0, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountMixedValidAndInvalid() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("VALID1"));
      bankStat.addTransaction(new BankStat.Deposit("DUPLICATE"));
      bankStat.addTransaction(new BankStat.Withdraw("DUPLICATE"));
      bankStat.addTransaction(new BankStat.Deposit("VALID2"));
      bankStat.addTransaction(new BankStat.Withdraw("TRIPLICATE"));
      bankStat.addTransaction(new BankStat.Deposit("TRIPLICATE"));
      bankStat.addTransaction(new BankStat.Withdraw("TRIPLICATE"));

      assertEquals(2, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountWithEmptyId() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw(""));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountWithEmptyDuplicateIds() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw(""));
      bankStat.addTransaction(new BankStat.Deposit(""));

      assertEquals(0, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountSameIdDifferentTransactionTypes() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("T123"));
      bankStat.addTransaction(new BankStat.Deposit("T123"));
      bankStat.addTransaction(new BankStat.Withdraw("T456"));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountAfterArrayGrowth() {
      var bankStat = new BankStat();
      for (var i = 0; i < 20; i++) {
        if (i < 10) {
          bankStat.addTransaction(new BankStat.Withdraw("UNIQUE" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("DUPLICATE" + (i % 5)));
        }
      }

      assertEquals(10, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountWithEmptyStringId() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw(""));
      bankStat.addTransaction(new BankStat.Deposit("NORMAL"));
      bankStat.addTransaction(new BankStat.Withdraw(""));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountLargeNumberOfTransactions() {
      var bankStat = new BankStat();
      for (var i = 0; i < 1_000_000; i++) {
        bankStat.addTransaction(new BankStat.Withdraw("UNIQUE" + i));
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        assertEquals(1_000_000, bankStat.validTransactionCount());
      });
    }

    @Test
    public void testValidTransactionCountWithVaryingDuplicateCounts() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("ONCE"));

      bankStat.addTransaction(new BankStat.Deposit("TWICE1"));
      bankStat.addTransaction(new BankStat.Withdraw("TWICE1"));

      bankStat.addTransaction(new BankStat.Withdraw("THRICE"));
      bankStat.addTransaction(new BankStat.Deposit("THRICE"));
      bankStat.addTransaction(new BankStat.Withdraw("THRICE"));

      bankStat.addTransaction(new BankStat.Deposit("FOUR"));
      bankStat.addTransaction(new BankStat.Withdraw("FOUR"));
      bankStat.addTransaction(new BankStat.Deposit("FOUR"));
      bankStat.addTransaction(new BankStat.Withdraw("FOUR"));

      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountConsistencyWithCountById() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Withdraw("VALID1"));
      bankStat.addTransaction(new BankStat.Deposit("VALID2"));
      bankStat.addTransaction(new BankStat.Withdraw("DUPLICATE"));
      bankStat.addTransaction(new BankStat.Deposit("DUPLICATE"));
      bankStat.addTransaction(new BankStat.Withdraw("VALID3"));

      assertEquals(3, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountDoesNotModifyState() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      var initialWithdrawCount = bankStat.withdrawCount();
      var initialDepositCount = bankStat.depositCount();

      var validCount = bankStat.validTransactionCount();
      assertEquals(2, validCount);

      assertEquals(initialWithdrawCount, bankStat.withdrawCount());
      assertEquals(initialDepositCount, bankStat.depositCount());
    }

    @Test
    public void testValidTransactionCountMultipleCalls() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));

      var firstCall = bankStat.validTransactionCount();
      var secondCall = bankStat.validTransactionCount();
      var thirdCall = bankStat.validTransactionCount();

      assertEquals(2, firstCall);
      assertEquals(2, secondCall);
      assertEquals(2, thirdCall);
    }

    @Test
    public void testValidTransactionCountAfterAddingMoreTransactions() {
      var bankStat = new BankStat();
      bankStat.addTransaction(new BankStat.Withdraw("W123"));
      bankStat.addTransaction(new BankStat.Deposit("D456"));
      assertEquals(2, bankStat.validTransactionCount());

      bankStat.addTransaction(new BankStat.Deposit("W123"));
      assertEquals(1, bankStat.validTransactionCount());

      bankStat.addTransaction(new BankStat.Withdraw("W789"));
      assertEquals(2, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountEdgeCaseAllSameId() {
      var bankStat = new BankStat();
      for (var i = 0; i < 10; i++) {
        if (i % 2 == 0) {
          bankStat.addTransaction(new BankStat.Withdraw("SAME_ID"));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("SAME_ID"));
        }
      }

      assertEquals(0, bankStat.validTransactionCount());
    }

    @Test
    public void testValidTransactionCountPerformanceWithManyTransactions() {
      var bankStat = new BankStat();
      for (var i = 0; i < 1_000_000; i++) {
        if (i < 500_000) {
          bankStat.addTransaction(new BankStat.Withdraw("ID" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("ID" + (i - 500_000)));
        }
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        assertEquals(0, bankStat.validTransactionCount());
      });
    }
  }


  @Nested
  public class Q9 {
    @Test
    public void testWithdrawIsValidTransactionCountOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Withdraw("W05"));
      bankStat.addTransaction(new BankStat.Withdraw("W17"));
      assertEquals(2, bankStat.validTransactionCount());

      bankStat.addTransaction(new BankStat.Withdraw("W05"));
      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testDepositIsValidTransactionCountOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Deposit("D1"));
      bankStat.addTransaction(new BankStat.Deposit("D2"));
      assertEquals(2, bankStat.validTransactionCount());

      bankStat.addTransaction(new BankStat.Deposit("D1"));
      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testMixedIsValidTransactionCountOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Withdraw("T1"));
      bankStat.addTransaction(new BankStat.Deposit("T2"));
      assertEquals(2, bankStat.validTransactionCount());

      bankStat.addTransaction(new BankStat.Withdraw("T1"));
      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testReplaceIdIsValidTransactionCountOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Withdraw("W05"));
      bankStat.addTransaction(new BankStat.Withdraw("W12"));
      bankStat.addTransaction(new BankStat.Withdraw("W17"));
      assertEquals(3, bankStat.validTransactionCount());

      bankStat.replaceId("W17"::equals, Map.of("W17", "W05")::get);
      assertEquals(1, bankStat.validTransactionCount());
    }

    @Test
    public void testContainsIdWithManyTransactions() {
      var bankStat = new BankStat();
      var totalTransactions = 1_000_000;

      for (var i = 0; i < totalTransactions; i++) {
        if (i < totalTransactions / 2) {
          bankStat.addTransaction(new BankStat.Withdraw("W" + i));
        } else {
          bankStat.addTransaction(new BankStat.Deposit("D" + i));
        }
      }

      assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        for (var i = 0; i < totalTransactions; i++) {
          if (i < totalTransactions / 2) {
            assertTrue(bankStat.containsId("W" + i));
          } else {
            assertTrue(bankStat.containsId("D" + i));
          }
        }
      });
    }

    @Test
    public void testWithdrawContainsIdOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Withdraw("W05"));
      bankStat.addTransaction(new BankStat.Withdraw("W17"));
      assertTrue(bankStat.containsId("W17"));

      bankStat.addTransaction(new BankStat.Withdraw("W03"));
      assertTrue(bankStat.containsId("W03"));
    }

    @Test
    public void testDepositContainsIdOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Deposit("D2"));
      bankStat.addTransaction(new BankStat.Deposit("D3"));
      assertTrue(bankStat.containsId("D2"));

      bankStat.addTransaction(new BankStat.Deposit("D1"));
      assertTrue(bankStat.containsId("D1"));
    }

    @Test
    public void testMixedContainsIdOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Deposit("T1"));
      bankStat.addTransaction(new BankStat.Withdraw("T3"));
      assertTrue(bankStat.containsId("T1"));

      bankStat.addTransaction(new BankStat.Deposit("T2"));
      assertTrue(bankStat.containsId("T2"));
    }

    @Test
    public void testReplaceIdContainsIdOptimizationWorks() {
      var bankStat = new BankStat();

      bankStat.addTransaction(new BankStat.Deposit("D2"));
      bankStat.addTransaction(new BankStat.Deposit("D3"));
      bankStat.addTransaction(new BankStat.Deposit("D4"));
      assertTrue(bankStat.containsId("D3"));

      bankStat.replaceId("D3"::equals, Map.of("D3", "D7")::get);
      assertFalse(bankStat.containsId("D3"));
      assertTrue(bankStat.containsId("D7"));
    }
  }
}