// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Foundation/Foundation.h>

#import "base/files/file_path.h"
#import "base/functional/bind.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/task_environment.h"
#import "ios/web/public/test/fakes/fake_download_task.h"
#import "ios/web_view/internal/cwv_download_task_internal.h"
#import "net/base/net_errors.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"

using base::test::ios::kWaitForFileOperationTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

namespace ios_web_view {

class CWVDownloadTaskTest : public PlatformTest {
 public:
  CWVDownloadTaskTest()
      : valid_local_file_path_(testing::TempDir() + "/foo.txt") {
    auto task_ptr = std::make_unique<web::FakeDownloadTask>(
        GURL("http://example.com/foo.txt"), "text/plain");
    fake_internal_task_ = task_ptr.get();
    cwv_task_ =
        [[CWVDownloadTask alloc] initWithInternalTask:std::move(task_ptr)];
    mock_delegate_ = OCMProtocolMock(@protocol(CWVDownloadTaskDelegate));
    cwv_task_.delegate = mock_delegate_;
  }

 protected:
  std::string valid_local_file_path_;
  base::test::TaskEnvironment task_environment_;
  web::FakeDownloadTask* fake_internal_task_ = nullptr;
  id<CWVDownloadTaskDelegate> mock_delegate_ = nil;
  CWVDownloadTask* cwv_task_ = nil;

  // Waits until fake_internal_task_->Start() is called.
  [[nodiscard]] bool WaitUntilTaskStarts() {
    return WaitUntilConditionOrTimeout(kWaitForFileOperationTimeout, ^{
      task_environment_.RunUntilIdle();
      return fake_internal_task_->GetState() ==
             web::DownloadTask::State::kInProgress;
    });
  }
};

// Tests a flow where the download starts and finishes successfully.
TEST_F(CWVDownloadTaskTest, SuccessfulFlow) {
  OCMExpect([mock_delegate_ downloadTaskProgressDidChange:cwv_task_]);
  [cwv_task_ startDownloadToLocalFileAtPath:base::SysUTF8ToNSString(
                                                valid_local_file_path_)];
  ASSERT_TRUE(WaitUntilTaskStarts());
  EXPECT_OCMOCK_VERIFY((id)mock_delegate_);

  OCMExpect([mock_delegate_ downloadTaskProgressDidChange:cwv_task_]);
  fake_internal_task_->SetPercentComplete(50);
  EXPECT_OCMOCK_VERIFY((id)mock_delegate_);

  OCMExpect([mock_delegate_ downloadTask:cwv_task_ didFinishWithError:nil]);
  fake_internal_task_->SetDone(true);
  EXPECT_OCMOCK_VERIFY((id)mock_delegate_);
}

// Tests a flow where the download finishes with an error.
TEST_F(CWVDownloadTaskTest, FailedFlow) {
  [cwv_task_ startDownloadToLocalFileAtPath:base::SysUTF8ToNSString(
                                                valid_local_file_path_)];
  ASSERT_TRUE(WaitUntilTaskStarts());

  OCMExpect([mock_delegate_
            downloadTask:cwv_task_
      didFinishWithError:[OCMArg checkWithBlock:^(NSError* error) {
        return error.code == CWVDownloadErrorFailed;
      }]]);
  fake_internal_task_->SetErrorCode(net::ERR_FAILED);
  fake_internal_task_->SetDone(true);
  EXPECT_OCMOCK_VERIFY((id)mock_delegate_);
}

// Tests a flow where the download is cancelled.
TEST_F(CWVDownloadTaskTest, CancelledFlow) {
  [cwv_task_ startDownloadToLocalFileAtPath:base::SysUTF8ToNSString(
                                                valid_local_file_path_)];
  ASSERT_TRUE(WaitUntilTaskStarts());

  OCMExpect([mock_delegate_
            downloadTask:cwv_task_
      didFinishWithError:[OCMArg checkWithBlock:^(NSError* error) {
        return error.code == CWVDownloadErrorAborted;
      }]]);

  [cwv_task_ cancel];
  EXPECT_EQ(web::DownloadTask::State::kCancelled,
            fake_internal_task_->GetState());

  // Simulate behavior of a real web::DownloadTask which transition to state
  // kComplete with error code net::ERR_ABORTED when cancelled.
  fake_internal_task_->SetErrorCode(net::ERR_ABORTED);
  fake_internal_task_->SetDone(true);

  EXPECT_OCMOCK_VERIFY((id)mock_delegate_);
}

// Tests a case when it fails to write to the specified local file path.
TEST_F(CWVDownloadTaskTest, WriteFailure) {
  __block bool did_finish_called = false;
  OCMStub([mock_delegate_ downloadTask:cwv_task_
                    didFinishWithError:[OCMArg isNotNil]])
      .andDo(^(NSInvocation*) {
        did_finish_called = true;
      });

  NSString* path =
      base::SysUTF8ToNSString(testing::TempDir() + "/non_existent_dir/foo.txt");
  [cwv_task_ startDownloadToLocalFileAtPath:path];
  // Simulate behavior of a real web::DownloadTask which transitions to state
  // net::ERR_ABORTED when a nonexistent directory is used as the path to write
  // to
  fake_internal_task_->SetErrorCode(net::ERR_ABORTED);
  fake_internal_task_->SetDone(true);

  EXPECT_TRUE(did_finish_called);
}

// Tests properties of CWVDownloadTask.
TEST_F(CWVDownloadTaskTest, Properties) {
  // Specified in the constructor.
  EXPECT_NSEQ([NSURL URLWithString:@"http://example.com/foo.txt"],
              cwv_task_.originalURL);
  EXPECT_NSEQ(@"text/plain", cwv_task_.MIMEType);

  fake_internal_task_->SetGeneratedFileName(
      base::FilePath(FILE_PATH_LITERAL("foo.txt")));
  EXPECT_NSEQ(@"foo.txt", cwv_task_.suggestedFileName);

  fake_internal_task_->SetTotalBytes(1024);
  EXPECT_EQ(1024, cwv_task_.totalBytes);

  fake_internal_task_->SetTotalBytes(-1);  // Unknown
  EXPECT_EQ(CWVDownloadSizeUnknown, cwv_task_.totalBytes);

  fake_internal_task_->SetReceivedBytes(512);
  EXPECT_EQ(512, cwv_task_.receivedBytes);

  fake_internal_task_->SetPercentComplete(50);
  EXPECT_FLOAT_EQ(0.5, cwv_task_.progress);

  fake_internal_task_->SetPercentComplete(-1);  // Unknown
  EXPECT_TRUE(isnan(cwv_task_.progress));
}

}  // namespace ios_web_view
