/*
 * Copyright 2020 The Error Prone Authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.errorprone.bugpatterns.time;

import static com.google.common.truth.Truth.assertThat;
import static java.time.Month.DECEMBER;
import static java.time.Month.FEBRUARY;
import static java.time.Month.JANUARY;
import static java.time.Month.JUNE;
import static java.time.Month.MAY;

import com.google.errorprone.CompilationTestHelper;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link DateChecker}. */
@RunWith(JUnit4.class)
@SuppressWarnings({"DateChecker", "JdkObsolete", "deprecation"})
public class DateCheckerTest {
  private final CompilationTestHelper helper =
      CompilationTestHelper.newInstance(DateChecker.class, getClass());

  @Test
  public void badBehavior() {
    assertThat(toLocalDate(2020, 6, 10)).isEqualTo(LocalDate.of(2020 + 1900, 6 + 1, 10));

    assertThat(toLocalDate(120, 0, 20)).isEqualTo(LocalDate.of(2020, JANUARY, 20));
    assertThat(toLocalDate(120, 12, 20)).isEqualTo(LocalDate.of(2021, JANUARY, 20));
    assertThat(toLocalDate(120, 13, 20)).isEqualTo(LocalDate.of(2021, FEBRUARY, 20));
    assertThat(toLocalDate(120, -1, 20)).isEqualTo(LocalDate.of(2019, DECEMBER, 20));
    assertThat(toLocalDate(121, -7, 20)).isEqualTo(LocalDate.of(2020, JUNE, 20));
    assertThat(toLocalDate(121, -7, 0)).isEqualTo(LocalDate.of(2020, MAY, 31));
    assertThat(toLocalDate(121, -7, -1)).isEqualTo(LocalDate.of(2020, MAY, 30));

    LocalDateTime expected = LocalDateTime.of(2020, JUNE, 20, 16, 36, 13);
    // normal (well, for some definition of "normal")
    assertThat(toLocalDateTime(120, 5, 20, 16, 36, 13)).isEqualTo(expected);
    // hours = 40 (wrap around)
    assertThat(toLocalDateTime(120, 5, 19, 40, 36, 13)).isEqualTo(expected);
    // minutes = 96 (wrap around)
    assertThat(toLocalDateTime(120, 5, 20, 15, 96, 13)).isEqualTo(expected);
    // seconds = 73 (wrap around)
    assertThat(toLocalDateTime(120, 5, 20, 16, 35, 73)).isEqualTo(expected);
  }

  private static LocalDate toLocalDate(int year, int month, int day) {
    return new Date(year, month, day).toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
  }

  private static LocalDateTime toLocalDateTime(
      int year, int month, int day, int hour, int minute, int second) {
    return new Date(year, month, day, hour, minute, second)
        .toInstant()
        .atZone(ZoneId.systemDefault())
        .toLocalDateTime();
  }

  @Test
  public void constructor_good() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import static java.util.Calendar.JULY;
            import java.util.Date;

            public class TestClass {
              Date good = new Date(120, JULY, 10);
            }
            """)
        .doTest();
  }

  @Test
  public void constructor_nonConstantMonth() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              // BUG: Diagnostic contains: Use Calendar.MAY instead of 4 to represent the month.
              Date good = new Date(120, 4, 10);
            }
            """)
        .doTest();
  }

  @Test
  public void constructor_constants() {
    // TODO(kak): We may want to consider warning on this case as well.
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              private static final int MAY = 4;
              Date good = new Date(120, MAY, 31);
            }
            """)
        .doTest();
  }

  @Test
  public void constructor_nonConstants() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              Date good = new Date(getYear(), getMonth(), getDay());

              int getYear() {
                return 120;
              }

              int getMonth() {
                return 0;
              }

              int getDay() {
                return 1;
              }
            }
            """)
        .doTest();
  }

  @Test
  public void constructor_allBad() {
    helper
        .addSourceLines(
            "TestClass.java",
"""
import java.util.Date;

public class TestClass {
  // BUG: Diagnostic contains: The 1900-based year value (2020) is out of bounds [1..300].  The
  // 0-based month value (13) is out of bounds [0..11].
  Date bad1 = new Date(2020, 13, 31);
  // BUG: Diagnostic contains: The 1900-based year value (2020) is out of bounds [1..300].  The
  // 0-based month value (13) is out of bounds [0..11].  The hours value (-2) is out of bounds
  // [0..23].  The minutes value (61) is out of bounds [0..59].
  Date bad2 = new Date(2020, 13, 31, -2, 61);
  // BUG: Diagnostic contains: The 1900-based year value (2020) is out of bounds [1..300].  The
  // 0-based month value (13) is out of bounds [0..11].  The hours value (-2) is out of bounds
  // [0..23].  The minutes value (61) is out of bounds [0..59].  The seconds value (75) is out of
  // bounds [0..59].
  Date bad3 = new Date(2020, 13, 31, -2, 61, 75);
}
""")
        .doTest();
  }

  @Test
  public void constructor_badYear() {
    helper
        .addSourceLines(
            "TestClass.java",
"""
import static java.util.Calendar.JULY;
import java.util.Date;

public class TestClass {
  // BUG: Diagnostic contains: The 1900-based year value (2020) is out of bounds [1..300].
  Date bad = new Date(2020, JULY, 10);
}
""")
        .doTest();
  }

  @Test
  public void constructor_badMonth() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              // BUG: Diagnostic contains: The 0-based month value (12) is out of bounds [0..11].
              Date bad1 = new Date(120, 12, 10);
              // BUG: Diagnostic contains: The 0-based month value (13) is out of bounds [0..11].
              Date bad2 = new Date(120, 13, 10);
              // BUG: Diagnostic contains: The 0-based month value (-1) is out of bounds [0..11].
              Date bad3 = new Date(120, -1, 10);
              // BUG: Diagnostic contains: The 0-based month value (-13) is out of bounds [0..11].
              Date bad4 = new Date(120, -13, 10);
              // BUG: Diagnostic contains: Use Calendar.MAY instead of 4 to represent the month.
              Date bad5 = new Date(120, 4, 10);
            }
            """)
        .doTest();
  }

  @Test
  public void constructor_badDay() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import static java.util.Calendar.JULY;
            import java.util.Date;

            public class TestClass {
              // BUG: Diagnostic contains: The day value (32) is out of bounds [1..31].
              Date bad1 = new Date(120, JULY, 32);
              // BUG: Diagnostic contains: The day value (0) is out of bounds [1..31].
              Date bad2 = new Date(120, JULY, 0);
              // BUG: Diagnostic contains: The day value (-32) is out of bounds [1..31].
              Date bad3 = new Date(120, JULY, -32);
            }
            """)
        .doTest();
  }

  @Test
  public void setters_good() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import static java.util.Calendar.*;
            import java.util.Date;

            public class TestClass {
              public void foo(Date date) {
                date.setYear(1);
                date.setYear(120);
                date.setYear(300);
                date.setMonth(JANUARY);
                date.setMonth(FEBRUARY);
                date.setMonth(MARCH);
                date.setMonth(APRIL);
                date.setMonth(MAY);
                date.setMonth(JUNE);
                date.setMonth(JULY);
                date.setMonth(AUGUST);
                date.setMonth(SEPTEMBER);
                date.setMonth(OCTOBER);
                date.setMonth(NOVEMBER);
                date.setMonth(DECEMBER);
                date.setDate(1);
                date.setDate(15);
                date.setDate(31);
                date.setHours(0);
                date.setHours(12);
                date.setHours(23);
                date.setMinutes(0);
                date.setMinutes(30);
                date.setMinutes(59);
                date.setSeconds(0);
                date.setSeconds(30);
                date.setSeconds(59);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void setters_badYears() {
    helper
        .addSourceLines(
            "TestClass.java",
"""
import java.util.Date;

public class TestClass {
  public void foo(Date date) {
    // BUG: Diagnostic contains: The 1900-based year value (0) is out of bounds [1..300].
    date.setYear(0);
    // BUG: Diagnostic contains: The 1900-based year value (-1) is out of bounds [1..300].
    date.setYear(-1);
    // BUG: Diagnostic contains: The 1900-based year value (2020) is out of bounds [1..300].
    date.setYear(2020);
  }
}
""")
        .doTest();
  }

  @Test
  public void setters_badMonths() {
    helper
        .addSourceLines(
            "TestClass.java",
"""
import java.util.Date;

public class TestClass {
  public void foo(Date date) {
    // BUG: Diagnostic contains: The 0-based month value (-13) is out of bounds [0..11].
    date.setMonth(-13);
    // BUG: Diagnostic contains: The 0-based month value (-1) is out of bounds [0..11].
    date.setMonth(-1);
    // BUG: Diagnostic contains: The 0-based month value (12) is out of bounds [0..11].
    date.setMonth(12);
  }
}
""")
        .doTest();
  }

  @Test
  public void setters_badDays() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              public void foo(Date date) {
                // BUG: Diagnostic contains: The day value (-32) is out of bounds [1..31].
                date.setDate(-32);
                // BUG: Diagnostic contains: The day value (0) is out of bounds [1..31].
                date.setDate(0);
                // BUG: Diagnostic contains: The day value (32) is out of bounds [1..31].
                date.setDate(32);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void setters_badHours() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              public void foo(Date date) {
                // BUG: Diagnostic contains: The hours value (-1) is out of bounds [0..23].
                date.setHours(-1);
                // BUG: Diagnostic contains: The hours value (24) is out of bounds [0..23].
                date.setHours(24);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void setters_badMinutes() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              public void foo(Date date) {
                // BUG: Diagnostic contains: The minutes value (-1) is out of bounds [0..59].
                date.setMinutes(-1);
                // BUG: Diagnostic contains: The minutes value (60) is out of bounds [0..59].
                date.setMinutes(60);
              }
            }
            """)
        .doTest();
  }

  @Test
  public void setters_badSeconds() {
    helper
        .addSourceLines(
            "TestClass.java",
            """
            import java.util.Date;

            public class TestClass {
              public void foo(Date date) {
                // BUG: Diagnostic contains: The seconds value (-1) is out of bounds [0..59].
                date.setSeconds(-1);
                // BUG: Diagnostic contains: The seconds value (60) is out of bounds [0..59].
                date.setSeconds(60);
              }
            }
            """)
        .doTest();
  }
}
