/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.beam.sdk.io.aws2.s3;

import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkArgument;
import static org.apache.beam.vendor.guava.v26_0_jre.com.google.common.base.Preconditions.checkNotNull;

import java.io.IOException;
import java.util.List;
import org.apache.beam.sdk.io.fs.MatchResult;
import org.apache.beam.sdk.io.fs.ResourceId;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.ImmutableList;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

/**
 * Hamcrest {@link Matcher} to match {@link MatchResult}. Necessary because {@link
 * MatchResult#metadata()} throws an exception under normal circumstances.
 */
@SuppressWarnings({
  "nullness" // TODO(https://github.com/apache/beam/issues/20497)
})
class MatchResultMatcher extends BaseMatcher<MatchResult> {

  private final MatchResult.Status expectedStatus;
  private final List<MatchResult.Metadata> expectedMetadata;
  private final IOException expectedException;

  private MatchResultMatcher(
      MatchResult.Status expectedStatus,
      List<MatchResult.Metadata> expectedMetadata,
      IOException expectedException) {
    this.expectedStatus = checkNotNull(expectedStatus);
    checkArgument((expectedMetadata == null) ^ (expectedException == null));
    this.expectedMetadata = expectedMetadata;
    this.expectedException = expectedException;
  }

  static MatchResultMatcher create(List<MatchResult.Metadata> expectedMetadata) {
    return new MatchResultMatcher(MatchResult.Status.OK, expectedMetadata, null);
  }

  private static MatchResultMatcher create(MatchResult.Metadata expectedMetadata) {
    return create(ImmutableList.of(expectedMetadata));
  }

  static MatchResultMatcher create(
      long sizeBytes, long lastModifiedMillis, ResourceId resourceId, boolean isReadSeekEfficient) {
    return create(
        MatchResult.Metadata.builder()
            .setSizeBytes(sizeBytes)
            .setLastModifiedMillis(lastModifiedMillis)
            .setResourceId(resourceId)
            .setIsReadSeekEfficient(isReadSeekEfficient)
            .build());
  }

  static MatchResultMatcher create(
      MatchResult.Status expectedStatus, IOException expectedException) {
    return new MatchResultMatcher(expectedStatus, null, expectedException);
  }

  static MatchResultMatcher create(MatchResult expected) {
    MatchResult.Status expectedStatus = expected.status();
    List<MatchResult.Metadata> expectedMetadata = null;
    IOException expectedException = null;
    try {
      expectedMetadata = expected.metadata();
    } catch (IOException e) {
      expectedException = e;
    }
    return new MatchResultMatcher(expectedStatus, expectedMetadata, expectedException);
  }

  @Override
  public boolean matches(Object actual) {
    if (actual == null) {
      return false;
    }
    if (!(actual instanceof MatchResult)) {
      return false;
    }
    MatchResult actualResult = (MatchResult) actual;
    if (!expectedStatus.equals(actualResult.status())) {
      return false;
    }

    List<MatchResult.Metadata> actualMetadata;
    try {
      actualMetadata = actualResult.metadata();
    } catch (IOException e) {
      return expectedException != null && expectedException.toString().equals(e.toString());
    }
    return expectedMetadata != null && expectedMetadata.equals(actualMetadata);
  }

  @Override
  public void describeTo(Description description) {
    if (expectedMetadata != null) {
      description.appendText(MatchResult.create(expectedStatus, expectedMetadata).toString());
    } else {
      description.appendText(MatchResult.create(expectedStatus, expectedException).toString());
    }
  }
}
