// 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.

import Foundation
import Thrift
import Common
import XCTest

enum Error: Int32 {
  case baseTypes = 1
  case structs = 2
  case containers = 4
  case exceptions = 8
  case unknown = 64
  case timeout = 128
}

class TestClient {
  var client: ThriftTestClient
  var resultCode: Int32 = 0

  public init(parameters: TestClientParameters) throws {
    let transport = try TestClient.getTransport(parameters: parameters)  
    let proto = try TestClient.getProtocol(parameters: parameters, transport: transport)
    client = ThriftTestClient(inoutProtocol: proto)
  }

  static func getTransport(parameters: TestClientParameters) throws -> TTransport {
    let socketTransport = try TSocketTransport(hostname: parameters.host!, port: parameters.port!)
    if parameters.transport == .framed {
      return TFramedTransport(transport: socketTransport)
    } 

    if parameters.transport == .buffered {
      return socketTransport
    }

    throw ParserError.unsupportedOption
  }

  static func getProtocol(parameters: TestClientParameters, transport: TTransport) throws -> TProtocol {
    if parameters.proto == .binary {
      return TBinaryProtocol(on: transport)
    } 

    if parameters.proto == .compact {
      return TCompactProtocol(on: transport)
    }

    throw ParserError.unsupportedOption
  }

  func run() throws {
    do {
      try testVoid()
      try testString()
      try testBool()
      try testByte()
      try testI32()
      try testI64()
      try testDouble()
      try testBinary()
      try testStruct()
      try testNest()
      try testMap()
      try testSet()
      try testList()
      try testEnum()
      try testTypedef()
      try testMapMap()
      try testInsanity()
      try testMulti()
      try testException()
      try testMultiException()
      // Swift generator doesn't yet support one way functions (THRIFT-5468)
      /*try testOneway()
       try testOnewayThenNormal()*/
      try testUuid()
      
    } catch let error {
      print("\(error)")
      resultCode |= Error.unknown.rawValue
    }
    exit(resultCode)
  }

  func testVoid() throws {
    print("testVoid")
    try client.testVoid()
  }


  func testString1(_ s1: String) throws {
    print("testString(\(s1))")
    let r1 = try client.testString(thing: s1)
    print(r1)
    if s1 != r1 {
      resultCode |= Error.baseTypes.rawValue
    }
  }

  func testString() throws {
    try testString1(String(repeating: "Python", count: 20))
    try testString1("")
    try testString1("\t\n/\\\\\r{}:パイソン")
    try testString1("""
Afrikaans, Alemannisch, Aragonés, العربية, مصرى,
Asturianu, Aymar aru, Azərbaycan, Башҡорт, Boarisch, Žemaitėška,
Беларуская, Беларуская (тарашкевіца), Български, Bamanankan,
বাংলা, Brezhoneg, Bosanski, Català, Mìng-dĕ̤ng-ngṳ̄, Нохчийн,
Cebuano, ᏣᎳᎩ, Česky, Словѣ́ньскъ / ⰔⰎⰑⰂⰡⰐⰠⰔⰍⰟ, Чӑвашла, Cymraeg,
Dansk, Zazaki, ދިވެހިބަސް, Ελληνικά, Emiliàn e rumagnòl, English,
Esperanto, Español, Eesti, Euskara, فارسی, Suomi, Võro, Føroyskt,
Français, Arpetan, Furlan, Frysk, Gaeilge, 贛語, Gàidhlig, Galego,
Avañe'ẽ, ગુજરાતી, Gaelg, עברית, हिन्दी, Fiji Hindi, Hrvatski,
Kreyòl ayisyen, Magyar, Հայերեն, Interlingua, Bahasa Indonesia,
Ilokano, Ido, Íslenska, Italiano, 日本語, Lojban, Basa Jawa,
ქართული, Kongo, Kalaallisut, ಕನ್ನಡ, 한국어, Къарачай-Малкъар,
Ripoarisch, Kurdî, Коми, Kernewek, Кыргызча, Latina, Ladino,
Lëtzebuergesch, Limburgs, Lingála, ລາວ, Lietuvių, Latviešu, Basa
Banyumasan, Malagasy, Македонски, മലയാളം, मराठी, مازِرونی, Bahasa
Melayu, Nnapulitano, Nedersaksisch, नेपाल भाषा, Nederlands, ‪
Norsk (nynorsk)‬, ‪Norsk (bokmål)‬, Nouormand, Diné bizaad,
Occitan, Иронау, Papiamentu, Deitsch, Polski, پنجابی, پښتو,
Norfuk / Pitkern, Português, Runa Simi, Rumantsch, Romani, Română,
Русский, Саха тыла, Sardu, Sicilianu, Scots, Sámegiella, Simple
English, Slovenčina, Slovenščina, Српски / Srpski, Seeltersk,
Svenska, Kiswahili, தமிழ், తెలుగు, Тоҷикӣ, ไทย, Türkmençe, Tagalog,
Türkçe, Татарча/Tatarça, Українська, اردو, Tiếng Việt, Volapük,
Walon, Winaray, 吴语, isiXhosa, ייִדיש, Yorùbá, Zeêuws, 中文,
Bân-lâm-gú, 粵語
""")
  }
  
  func testBool1(_ s1: Bool) throws {
    print("testBool(\(s1))")
    let r1 = try client.testBool(thing: s1)
    print(r1)
    if s1 != r1 {
      resultCode |= Error.baseTypes.rawValue
    }
  }
  
  func testBool() throws {
    try testBool1(true)
    try testBool1(false)
  }
  
  func testByte() throws {
    print("testByte")
    if try client.testByte(thing: 63) != 63 {
      resultCode |= Error.baseTypes.rawValue
    }
    if try client.testByte(thing: -127) != -127 {
      resultCode |= Error.baseTypes.rawValue
    }
  }
  
  func testI32() throws {
    print("testI32")
    if try client.testI32(thing: -1) != -1 {
      resultCode |= Error.baseTypes.rawValue
    }
    if try client.testI32(thing: 0) != 0 {
      resultCode |= Error.baseTypes.rawValue
    }
  }
  
  func testI64() throws {
    print("testI64")
    if try client.testI64(thing: 1) != 1 ||
        client.testI64(thing: -34359738368) != -34359738368 {
      resultCode |= Error.baseTypes.rawValue
    }
  }
  
  func testDouble() throws {
    print("testDouble")
    for testValue in [-5.235098235, 0, -1, -0.000341012439638598279] {
      if try client.testDouble(thing: testValue) != testValue {
        resultCode |= Error.baseTypes.rawValue
      }
    }
  }
  
  func testBinary() throws {
    print("testBinary")
    let val = Data(Array(0...255))
    if try client.testBinary(thing: val) != val {
      resultCode |= Error.baseTypes.rawValue
    }
  }
  
  func testStruct() throws {
    print("testStruct")
    let x = Xtruct(string_thing: "Zero", byte_thing: 1, i32_thing: -3, i64_thing: -5)
    if try client.testStruct(thing: x) != x {
      resultCode |= Error.structs.rawValue
    }
  }
  
  func testNest() throws {
    print("testNest")
    let inner = Xtruct(string_thing: "Zero", byte_thing: 1, i32_thing: -3, i64_thing: -5)
    let x = Xtruct2(byte_thing: 0, struct_thing: inner, i32_thing: 0)
    if try client.testNest(thing: x) != x {
      resultCode |= Error.structs.rawValue
    }
  }
  
  func testMap() throws {
    print("testMap")
    let x = TMap<Int32, Int32>([0: 1, 1: 2, 2: 3, 3: 4, -1: -2])
    if try client.testMap(thing: x) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testSet() throws {
    print("testSet")
    let x = TSet<Int32>([8, 1, 42])
    if try client.testSet(thing: x) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testList() throws {
    print("testList")
    let x = TList<Int32>([1, 4, 9, -42])
    if try client.testList(thing: x) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testEnum() throws {
    print("testEnum")
    let x = Numberz.five
    if try client.testEnum(thing: x) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testTypedef() throws {
    print("testTypedef")
    let x = UserId(bitPattern: 0xffffffffffffff)
    if try client.testTypedef(thing: x) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testMapMap() throws {
    print("testMapMap")
    let x = TMap<Int32, TMap<Int32, Int32>>([
      -4: [-4: -4, -3: -3, -2: -2, -1: -1],
       4: [4: 4, 3: 3, 2: 2, 1: 1]
    ])
    if try client.testMapMap(hello: 42) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testInsanity() throws {
    print("testInsanity()")
    let argument = Insanity(userMap: [.eight: 8], xtructs: [])
    let expected = TMap<UserId, TMap<Numberz, Insanity>>([
      1: [
        .two: argument,
        .three: argument
      ],
      2: [
        .six: Insanity(userMap: [:], xtructs: [])
      ]
    ])
    if try client.testInsanity(argument: argument) != expected {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testMulti() throws {
    print("testMulti")
    let x = Xtruct(string_thing: "Hello2", byte_thing: 74, i32_thing: 0xff00ff, i64_thing: 0xffffffffd0d0)
    if try client.testMulti(arg0: x.byte_thing, arg1: x.i32_thing, arg2: x.i64_thing, arg3: .init([0: "abc"]), arg4: Numberz.five, arg5: 0xf0f0f0) != x {
      resultCode |= Error.containers.rawValue
    }
  }
  
  func testException() throws {
    print("testException")
    try client.testException(arg: "Safe")
    do {
      try client.testException(arg: "Xception")
      resultCode |= Error.exceptions.rawValue
    } catch let error as Xception {
      guard error.errorCode == 1001, error.message == "Xception" else {
        resultCode |= Error.exceptions.rawValue
        return
      }
    } catch {
      resultCode |= Error.exceptions.rawValue
    }
    
    do {
      try client.testException(arg: "TException")
      resultCode |= Error.exceptions.rawValue
    } catch is TError {
      
    } catch {
      resultCode |= Error.exceptions.rawValue
    }
    
    try client.testException(arg: "success")
  }
  
  func testMultiException() throws {
    print("testMultiException")
    do {
      _ = try client.testMultiException(arg0: "Xception", arg1: "ignore")
    } catch let error as Xception {
      guard error.errorCode == 1001, error.message == "This is an Xception" else {
        resultCode |= Error.exceptions.rawValue
        return
      }
    } catch {
      resultCode |= Error.exceptions.rawValue
    }
    
    do {
      _ = try client.testMultiException(arg0: "Xception2", arg1: "ignore")
    } catch let error as Xception2 {
      guard error.errorCode == 2002, error.struct_thing.string_thing == "This is an Xception2" else {
        resultCode |= Error.exceptions.rawValue
        return
      }
    }
    
    let y = try client.testMultiException(arg0: "success", arg1: "foobar")
    if y.string_thing != "foobar" {
      resultCode |= Error.exceptions.rawValue
    }
  }
  
  // Swift generator doesn't yet support one way functions (THRIFT-5468)
  /*func testOneway() throws {
   print("testOneway")
   let start = CACurrentMediaTime()
   try client.testOneway(secondsToSleep: 1)
   let end = CACurrentMediaTime()
   let duration = end - start
   let delta = abs(1 - duration)
   print("oneway sleep took \(end - start) sec")
   
   guard delta < 0.5 else {
   print("oneway sleep took \(end - start) sec")
   resultCode |= Error.unknown.rawValue
   return
   }
   }
   
   func testOnewayThenNormal() throws {
   print("testOnewayThenNormal")
   try client.testOneway(secondsToSleep: 1)
   if try client.testString(thing: "Swift") != "Swift" {
   resultCode |= Error.baseTypes.rawValue
   }
   }*/
  
  func testUuid() throws {
    let uuid = UUID()
    guard try client.testUuid(thing: uuid) == uuid else {
      resultCode |= Error.baseTypes.rawValue
      return
    }
  }
}


let parameters = try TestClientParameters(arguments: CommandLine.arguments)
    
if parameters.showHelp {
  parameters.printHelp()
  exit(0)
}

Thread.sleep(forTimeInterval: 1)

try TestClient(parameters: parameters).run()
