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

#import "ios/web/public/test/web_view_content_test_util.h"

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#import <map>

#import "base/functional/bind.h"
#import "base/no_destructor.h"
#import "base/run_loop.h"
#import "base/strings/stringprintf.h"
#import "base/strings/sys_string_conversions.h"
#import "base/values.h"
#import "ios/web/find_in_page/find_in_page_java_script_feature.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/test/web_view_interaction_test_util.h"
#import "ios/web/public/web_state.h"
#import "net/base/apple/url_conversions.h"
#import "url/gurl.h"

using base::test::ios::kWaitForDownloadTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

// A helper delegate class that allows downloading responses with invalid
// SSL certs.
@interface TestURLSessionDelegate : NSObject <NSURLSessionDelegate>
@end

@implementation TestURLSessionDelegate

- (void)URLSession:(NSURLSession*)session
    didReceiveChallenge:(NSURLAuthenticationChallenge*)challenge
      completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition,
                                  NSURLCredential*))completionHandler {
  SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
  completionHandler(NSURLSessionAuthChallengeUseCredential,
                    [NSURLCredential credentialForTrust:serverTrust]);
}

@end

namespace {
// Script that returns the document contents as a string.
char kGetDocumentBodyJavaScript[] =
    "function allTextContent(element) { "
    "  if (!element) { return ''; }"
    "  let textString = element.textContent;"
    "  if (element.querySelectorAll) {"
    "    for (let e of element.querySelectorAll('*')) {"
    "      if (e && e.shadowRoot) {"
    "        textString += '|' + allTextContent(e.shadowRoot);"
    "      }"
    "    }"
    "  }"
    "  return textString;"
    "}"
    "allTextContent(document.body);";

// Fetches the image from `image_url`.
UIImage* LoadImage(const GURL& image_url) {
  __block UIImage* image;
  __block NSError* error;
  TestURLSessionDelegate* session_delegate =
      [[TestURLSessionDelegate alloc] init];
  NSURLSessionConfiguration* session_config =
      [NSURLSessionConfiguration ephemeralSessionConfiguration];
  NSURLSession* session =
      [NSURLSession sessionWithConfiguration:session_config
                                    delegate:session_delegate
                               delegateQueue:nil];
  id completion_handler = ^(NSData* data, NSURLResponse*, NSError* task_error) {
    error = task_error;
    image = [[UIImage alloc] initWithData:data];
  };

  NSURLSessionDataTask* task =
      [session dataTaskWithURL:net::NSURLWithGURL(image_url)
             completionHandler:completion_handler];
  [task resume];

  bool task_completed = WaitUntilConditionOrTimeout(kWaitForDownloadTimeout, ^{
    return image || error;
  });

  if (!task_completed) {
    return nil;
  }
  return image;
}
}  // namespace

using base::test::ios::kWaitForJSCompletionTimeout;
using base::test::ios::kWaitForUIElementTimeout;
using base::test::ios::WaitUntilConditionOrTimeout;

namespace web {
namespace test {

bool IsWebViewContainingText(web::WebState* web_state,
                             const std::string& text) {
  std::unique_ptr<base::Value> value =
      web::test::ExecuteJavaScript(web_state, kGetDocumentBodyJavaScript);
  std::string body;
  if (value && value->is_string()) {
    return value->GetString().contains(text);
  }
  return false;
}

bool IsWebViewContainingTextInFrame(web::WebState* web_state,
                                    const std::string& text) {
  __block NSInteger number_frames_processing = 0;
  __block bool text_found = false;
  for (WebFrame* frame :
       web_state->GetPageWorldWebFramesManager()->GetAllWebFrames()) {
    number_frames_processing++;

    FindInPageJavaScriptFeature* find_in_page_feature =
        FindInPageJavaScriptFeature::GetInstance();
    find_in_page_feature->Search(
        frame, text, base::BindOnce(^(std::optional<int> result_matches) {
          if (result_matches && result_matches.value() >= 1) {
            text_found = true;
          }
          number_frames_processing--;
        }));
  }
  bool success = WaitUntilConditionOrTimeout(kWaitForJSCompletionTimeout, ^{
    if (text_found) {
      return true;
    }
    return number_frames_processing == 0;
  });
  return text_found && success;
}

bool WaitForWebViewContainingText(web::WebState* web_state,
                                  std::string text,
                                  base::TimeDelta timeout) {
  return WaitUntilConditionOrTimeout(timeout, ^{
    base::RunLoop().RunUntilIdle();
    return IsWebViewContainingText(web_state, text);
  });
}

bool WaitForWebViewNotContainingText(web::WebState* web_state,
                                     std::string text,
                                     base::TimeDelta timeout) {
  return WaitUntilConditionOrTimeout(timeout, ^{
    base::RunLoop().RunUntilIdle();
    return !IsWebViewContainingText(web_state, text);
  });
}

bool WaitForWebViewContainingTextInFrame(web::WebState* web_state,
                                         std::string text,
                                         base::TimeDelta timeout) {
  return WaitUntilConditionOrTimeout(timeout, ^{
    base::RunLoop().RunUntilIdle();
    return IsWebViewContainingTextInFrame(web_state, text);
  });
}

bool IsWebViewContainingImage(std::string image_id,
                              web::WebState* web_state,
                              ImageStateElement image_state) {
  std::string get_url_script =
      base::StringPrintf("document.getElementById('%s').src", image_id.c_str());
  std::unique_ptr<base::Value> url_as_value =
      web::test::ExecuteJavaScript(web_state, get_url_script);
  if (!url_as_value || !url_as_value->is_string()) {
    return false;
  }
  std::string image_url = url_as_value->GetString();

  static base::NoDestructor<std::map<std::string, CGSize>> expected_sizes;
  CGSize expected_size;
  auto it = expected_sizes->find(image_url);
  if (it == expected_sizes->end()) {
    UIImage* image = LoadImage(GURL(image_url));
    if (!image) {
      return false;
    }
    expected_size = image.size;
    expected_sizes->insert({image_url, expected_size});
  } else {
    expected_size = it->second;
  }

  NSString* const kGetElementAttributesScript =
      [NSString stringWithFormat:@"var image = document.getElementById('%@');"
                                 @"var imageHeight = image.height;"
                                 @"var imageWidth = image.width;"
                                 @"JSON.stringify({"
                                 @"  height:imageHeight,"
                                 @"  width:imageWidth"
                                 @"});",
                                 base::SysUTF8ToNSString(image_id)];
  std::unique_ptr<base::Value> value = web::test::ExecuteJavaScript(
      web_state, base::SysNSStringToUTF8(kGetElementAttributesScript));
  if (value && value->is_string()) {
    NSString* evaluation_result = base::SysUTF8ToNSString(value->GetString());
    NSData* image_attributes_as_data =
        [evaluation_result dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary* image_attributes =
        [NSJSONSerialization JSONObjectWithData:image_attributes_as_data
                                        options:0
                                          error:nil];
    if (image_attributes) {
      CGFloat height = [image_attributes[@"height"] floatValue];
      CGFloat width = [image_attributes[@"width"] floatValue];
      switch (image_state) {
        case IMAGE_STATE_BLOCKED:
          return height < expected_size.height && width < expected_size.width;
        case IMAGE_STATE_LOADED:
          return height == expected_size.height && width == expected_size.width;
      }
    }
  }
  return false;
}

bool IsWebViewContainingElement(web::WebState* web_state,
                                ElementSelector* selector) {
  // Script that tests presence of element.
  std::string script = base::SysNSStringToUTF8(
      [NSString stringWithFormat:@"!!(%@)", selector.selectorScript]);

  std::unique_ptr<base::Value> value =
      web::test::ExecuteJavaScript(web_state, script);
  if (!value) {
    return false;
  }
  return value->GetIfBool().value_or(false);
}

bool WaitForWebViewContainingElement(web::WebState* web_state,
                                     ElementSelector* selector) {
  return WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return IsWebViewContainingElement(web_state, selector);
  });
}

bool WaitForWebViewNotContainingElement(web::WebState* web_state,
                                        ElementSelector* selector) {
  return WaitUntilConditionOrTimeout(kWaitForUIElementTimeout, ^{
    base::RunLoop().RunUntilIdle();
    return !IsWebViewContainingElement(web_state, selector);
  });
}

}  // namespace test
}  // namespace web
