import Mockingbird
@testable import MockingbirdTestsHost
import XCTest

@objc class MinimalObjCClass: Foundation.NSObject {
  @objc dynamic func method(valueType: Bool) -> Bool { fatalError() }
  @objc dynamic func method(bridgedType: String) -> String { fatalError() }
  @objc dynamic func method(referenceType: Foundation.NSObject) -> Foundation.NSObject {
    fatalError()
  }
  @objc dynamic func method(first: String, second: String) -> String { fatalError() }
  
  @objc dynamic func throwingMethod() throws {}
  @objc dynamic public func trivialMethod() {}
  
  @objc dynamic func methodReturningOptionalValue() -> String? { fatalError() }
  @objc dynamic var optionalProperty: String? = nil
  
  @objc dynamic var valueTypeProperty = false
  @objc dynamic var bridgedTypeProperty = ""
  @objc dynamic var referenceTypeProperty = Foundation.NSObject()
}

class MinimalObjCSubclass: MinimalObjCClass {}

class DynamicSwiftTests: BaseTestCase {
  
  var classMock: MinimalObjCClass!
  var subclassMock: MinimalObjCSubclass!
  
  override func setUpWithError() throws {
    self.classMock = mock(MinimalObjCClass.self)
    self.subclassMock = mock(MinimalObjCSubclass.self)
  }
  
  
  // MARK: - Swift Properties
  
  // MARK: Value types
  
  func testClassValueTypePropertyGetter() throws {
    given(classMock.valueTypeProperty).willReturn(true)
    XCTAssertTrue(classMock.valueTypeProperty)
    verify(classMock.valueTypeProperty).wasCalled()
  }
  func testClassValueTypePropertyGetter_stubbingOperator() throws {
    given(classMock.valueTypeProperty) ~> true
    XCTAssertTrue(classMock.valueTypeProperty)
    verify(classMock.valueTypeProperty).wasCalled()
  }
  
  func testClassValueTypePropertySetter() throws {
    let expectation = XCTestExpectation()
    given(classMock.valueTypeProperty = true).will { (newValue: Bool) in
      XCTAssertTrue(newValue)
      expectation.fulfill()
    }
    classMock.valueTypeProperty = true
    verify(classMock.valueTypeProperty = true).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassValueTypePropertySetter_stubbingOperator() throws {
    let expectation = XCTestExpectation()
    given(classMock.valueTypeProperty = true) ~> { (newValue: Bool) in
      XCTAssertTrue(newValue)
      expectation.fulfill()
    }
    classMock.valueTypeProperty = true
    verify(classMock.valueTypeProperty = true).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  func testClassValueTypePropertySetterMatchesWildcard() throws {
    let expectation = XCTestExpectation()
    given(classMock.valueTypeProperty = any()).will { (newValue: Bool) in
      XCTAssertTrue(newValue)
      expectation.fulfill()
    }
    classMock.valueTypeProperty = true
    verify(classMock.valueTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassValueTypePropertySetterMatchesWildcard_stubbingOperator() throws {
    let expectation = XCTestExpectation()
    given(classMock.valueTypeProperty = any()) ~> { (newValue: Bool) in
      XCTAssertTrue(newValue)
      expectation.fulfill()
    }
    classMock.valueTypeProperty = true
    verify(classMock.valueTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  // MARK: Bridged types
  
  func testClassBridgedPropertyGetter() throws {
    given(classMock.bridgedTypeProperty).willReturn("Ryan")
    XCTAssertEqual(classMock.bridgedTypeProperty, "Ryan")
    verify(classMock.bridgedTypeProperty).wasCalled()
  }
  func testClassBridgedPropertyGetter_stubbingOperator() throws {
    given(classMock.bridgedTypeProperty) ~> "Ryan"
    XCTAssertEqual(classMock.bridgedTypeProperty, "Ryan")
    verify(classMock.bridgedTypeProperty).wasCalled()
  }
  
  func testClassBridgedPropertySetter() throws {
    let expectation = XCTestExpectation()
    given(classMock.bridgedTypeProperty = "Ryan").will { (newValue: String) in
      XCTAssertEqual(newValue, "Ryan")
      expectation.fulfill()
    }
    classMock.bridgedTypeProperty = "Ryan"
    verify(classMock.bridgedTypeProperty = "Ryan").wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassBridgedPropertySetter_stubbingOperator() throws {
    let expectation = XCTestExpectation()
    given(classMock.bridgedTypeProperty = "Ryan") ~> { (newValue: String) in
      XCTAssertEqual(newValue, "Ryan")
      expectation.fulfill()
    }
    classMock.bridgedTypeProperty = "Ryan"
    verify(classMock.bridgedTypeProperty = "Ryan").wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  func testClassBridgedPropertySetterMatchesWildcard() throws {
    let expectation = XCTestExpectation()
    given(classMock.bridgedTypeProperty = any()).will { (newValue: String) in
      XCTAssertEqual(newValue, "Ryan")
      expectation.fulfill()
    }
    classMock.bridgedTypeProperty = "Ryan"
    verify(classMock.bridgedTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassBridgedPropertySetterMatchesWildcard_stubbingOperator() throws {
    let expectation = XCTestExpectation()
    given(classMock.bridgedTypeProperty = any()) ~> { (newValue: String) in
      XCTAssertEqual(newValue, "Ryan")
      expectation.fulfill()
    }
    classMock.bridgedTypeProperty = "Ryan"
    verify(classMock.bridgedTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  // MARK: Reference types
  
  func testClassReferenceTypePropertyGetter() throws {
    let ref = Foundation.NSObject()
    given(classMock.referenceTypeProperty).willReturn(ref)
    XCTAssertTrue(classMock.referenceTypeProperty === ref)
    verify(classMock.referenceTypeProperty).wasCalled()
  }
  func testClassReferenceTypePropertyGetter_stubbingOperator() throws {
    let ref = Foundation.NSObject()
    given(classMock.referenceTypeProperty) ~> ref
    XCTAssertTrue(classMock.referenceTypeProperty === ref)
    verify(classMock.referenceTypeProperty).wasCalled()
  }
  
  func testClassReferenceTypePropertySetter() throws {
    let ref = Foundation.NSObject()
    let expectation = XCTestExpectation()
    given(classMock.referenceTypeProperty = ref).will { (newValue: Foundation.NSObject) in
      XCTAssertEqual(newValue, ref)
      expectation.fulfill()
    }
    classMock.referenceTypeProperty = ref
    verify(classMock.referenceTypeProperty = ref).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassReferenceTypePropertySetter_stubbingOperator() throws {
    let ref = Foundation.NSObject()
    let expectation = XCTestExpectation()
    given(classMock.referenceTypeProperty = ref) ~> { (newValue: Foundation.NSObject) in
      XCTAssertEqual(newValue, ref)
      expectation.fulfill()
    }
    classMock.referenceTypeProperty = ref
    verify(classMock.referenceTypeProperty = ref).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  func testClassReferenceTypePropertySetterExclusive() throws {
    given(self.classMock.referenceTypeProperty = Foundation.NSObject()).will {
      (newValue: Foundation.NSObject) in
      XCTFail()
    }
    self.classMock.referenceTypeProperty = Foundation.NSObject()
  }
  func testClassReferenceTypePropertySetterExclusive_stubbingOperator() throws {
    given(self.classMock.referenceTypeProperty = Foundation.NSObject()) ~> {
      (newValue: Foundation.NSObject) in
      XCTFail()
    }
    self.classMock.referenceTypeProperty = Foundation.NSObject()
  }
  
  func testClassReferenceTypePropertySetterMatchesWildcard() throws {
    let ref = Foundation.NSObject()
    let expectation = XCTestExpectation()
    given(classMock.referenceTypeProperty = any()).will {
      (newValue: Foundation.NSObject) in
      XCTAssertEqual(newValue, ref)
      expectation.fulfill()
    }
    classMock.referenceTypeProperty = ref
    verify(classMock.referenceTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  func testClassReferenceTypePropertySetterMatchesWildcard_stubbingOperator() throws {
    let ref = Foundation.NSObject()
    let expectation = XCTestExpectation()
    given(classMock.referenceTypeProperty = any()) ~> {
      (newValue: Foundation.NSObject) in
      XCTAssertEqual(newValue, ref)
      expectation.fulfill()
    }
    classMock.referenceTypeProperty = ref
    verify(classMock.referenceTypeProperty = any()).wasCalled()
    wait(for: [expectation], timeout: 2)
  }
  
  
  // MARK: - Swift methods
  
  // MARK: Value types
  
  func testClassValueTypeMethodMatchesExact() throws {
    given(classMock.method(valueType: true)).willReturn(true)
    XCTAssertTrue(classMock.method(valueType: true))
    verify(classMock.method(valueType: true)).wasCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  func testClassValueTypeMethodMatchesExact_stubbingOperator() throws {
    given(classMock.method(valueType: true)) ~> true
    XCTAssertTrue(classMock.method(valueType: true))
    verify(classMock.method(valueType: true)).wasCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  
  func testClassValueTypeMethodMatchesExactExclusive() throws {
    shouldFail {
      given(self.classMock.method(valueType: true)).willReturn(true)
      _ = self.classMock.method(valueType: false)
    }
  }
  func testClassValueTypeMethodMatchesExactExclusive_stubbingOperator() throws {
    shouldFail {
      given(self.classMock.method(valueType: true)) ~> true
      _ = self.classMock.method(valueType: false)
    }
  }
  
  func testClassValueTypeMethodMatchesWildcard() throws {
    given(classMock.method(valueType: firstArg(any()))).will {
      (valueType: Bool) in
      return valueType
    }
    XCTAssertTrue(classMock.method(valueType: true))
    XCTAssertFalse(classMock.method(valueType: false))
    verify(classMock.method(valueType: true)).wasCalled()
    verify(classMock.method(valueType: false)).wasCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  func testClassValueTypeMethodMatchesWildcard_stubbingOperator() throws {
    given(classMock.method(valueType: any())) ~> {
      (valueType: Bool) in
      return valueType
    }
    XCTAssertTrue(classMock.method(valueType: true))
    XCTAssertFalse(classMock.method(valueType: false))
    verify(classMock.method(valueType: true)).wasCalled()
    verify(classMock.method(valueType: false)).wasCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  
  // MARK: Bridged types
  
  func testClassBridgedTypeMethodMatchesExact() throws {
    given(classMock.method(bridgedType: "Ryan")).willReturn("Ryan")
    XCTAssertEqual(classMock.method(bridgedType: "Ryan"), "Ryan")
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: "Ryan")).wasCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  func testClassBridgedTypeMethodMatchesExact_stubbingOperator() throws {
    given(classMock.method(bridgedType: "Ryan")) ~> "Ryan"
    XCTAssertEqual(classMock.method(bridgedType: "Ryan"), "Ryan")
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: "Ryan")).wasCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  
  func testClassBridgedTypeMethodMatchesExactExclusive() throws {
    shouldFail {
      given(self.classMock.method(bridgedType: "Ryan")).willReturn("Ryan")
      _ = self.classMock.method(bridgedType: "Sterling")
    }
  }
  func testClassBridgedTypeMethodMatchesExactExclusive_stubbingOperator() throws {
    shouldFail {
      given(self.classMock.method(bridgedType: "Ryan")) ~> "Ryan"
      _ = self.classMock.method(bridgedType: "Sterling")
    }
  }
  
  func testClassBridgedTypeMethodMatchesWildcard() throws {
    given(classMock.method(bridgedType: any())).will {
      (valueType: String) in
      return valueType
    }
    XCTAssertEqual(classMock.method(bridgedType: "Ryan"), "Ryan")
    XCTAssertEqual(classMock.method(bridgedType: "Sterling"), "Sterling")
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: "Ryan")).wasCalled()
    verify(classMock.method(bridgedType: "Sterling")).wasCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  func testClassBridgedTypeMethodMatchesWildcard_stubbingOperator() throws {
    given(classMock.method(bridgedType: any())) ~> {
      (valueType: String) in
      return valueType
    }
    XCTAssertEqual(classMock.method(bridgedType: "Ryan"), "Ryan")
    XCTAssertEqual(classMock.method(bridgedType: "Sterling"), "Sterling")
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: "Ryan")).wasCalled()
    verify(classMock.method(bridgedType: "Sterling")).wasCalled()
    verify(classMock.method(referenceType: any())).wasNeverCalled()
  }
  
  // MARK: Reference types
  
  func testClassReferenceTypeMethodMatchesExact() throws {
    let ref = Foundation.NSObject()
    given(classMock.method(referenceType: ref)).willReturn(ref)
    XCTAssertEqual(classMock.method(referenceType: ref), ref)
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: ref)).wasCalled()
  }
  func testClassReferenceTypeMethodMatchesExact_stubbingOperator() throws {
    let ref = Foundation.NSObject()
    given(classMock.method(referenceType: ref)) ~> ref
    XCTAssertEqual(classMock.method(referenceType: ref), ref)
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: ref)).wasCalled()
  }
  
  func testClassReferenceTypeMethodMatchesExactExclusive() throws {
    shouldFail {
      let ref = Foundation.NSObject()
      given(self.classMock.method(referenceType: ref)).willReturn(ref)
      _ = self.classMock.method(referenceType: Foundation.NSObject())
    }
  }
  func testClassReferenceTypeMethodMatchesExactExclusive_stubbingOperator() throws {
    shouldFail {
      let ref = Foundation.NSObject()
      given(self.classMock.method(referenceType: ref)) ~> ref
      _ = self.classMock.method(referenceType: Foundation.NSObject())
    }
  }
  
  func testClassReferenceTypeMethodMatchesWildcard() throws {
    given(classMock.method(referenceType: any())).will {
      (valueType: Foundation.NSObject) in
      return valueType
    }
    let ref1 = Foundation.NSObject()
    let ref2 = Foundation.NSObject()
    XCTAssertEqual(classMock.method(referenceType: ref1), ref1)
    XCTAssertEqual(classMock.method(referenceType: ref2), ref2)
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasCalled(twice)
  }
  func testClassReferenceTypeMethodMatchesWildcard_stubbingOperator() throws {
    given(classMock.method(referenceType: any())) ~> {
      (valueType: Foundation.NSObject) in
      return valueType
    }
    let ref1 = Foundation.NSObject()
    let ref2 = Foundation.NSObject()
    XCTAssertEqual(classMock.method(referenceType: ref1), ref1)
    XCTAssertEqual(classMock.method(referenceType: ref2), ref2)
    verify(classMock.method(valueType: any())).wasNeverCalled()
    verify(classMock.method(bridgedType: any())).wasNeverCalled()
    verify(classMock.method(referenceType: any())).wasCalled(twice)
  }
  
  // MARK: Multiple parameters
  
  func testClassMultipleParameterMethod_matchesHomogenousWildcard() throws {
    given(classMock.method(first: any(), second: any())).will {
      (first: String, second: String) in
      return first + "-" + second
    }
    XCTAssertEqual(classMock.method(first: "a", second: "b"), "a-b")
    verify(classMock.method(first: any(), second: any())).wasCalled()
  }
  
  func testClassMultipleParameterMethod_matchesHomogenousWildcard_explicitIndexes() throws {
    given(classMock.method(first: firstArg(any()), second: secondArg(any()))).will {
      (first: String, second: String) in
      return first + "-" + second
    }
    XCTAssertEqual(classMock.method(first: "a", second: "b"), "a-b")
    verify(classMock.method(first: firstArg(any()), second: secondArg(any()))).wasCalled()
  }
  
  func testClassMultipleParameterMethod_matchesHeterogenousWildcardFirst_explicitIndexes() throws {
    given(classMock.method(first: "a", second: secondArg(any()))).will {
      (first: String, second: String) in
      return first + "-" + second
    }
    XCTAssertEqual(classMock.method(first: "a", second: "b"), "a-b")
    verify(classMock.method(first: "a", second: secondArg(any()))).wasCalled()
  }
  
  func testClassMultipleParameterMethod_matchesHeterogenousWildcardSecond_explicitIndexes() throws {
    given(classMock.method(first: firstArg(any()), second: "b")).will {
      (first: String, second: String) in
      return first + "-" + second
    }
    XCTAssertEqual(classMock.method(first: "a", second: "b"), "a-b")
    verify(classMock.method(first: firstArg(any()), second: "b")).wasCalled()
  }
  
  func testClassMultipleParameterMethod_failsStubbingHeterogenous() throws {
    shouldFail {
      given(self.classMock.method(first: "a", second: any())).willReturn("foo")
    }
    shouldFail {
      given(self.classMock.method(first: any(), second: "b")).willReturn("foo")
    }
  }
  
  func testClassMultipleParameterMethod_failsVerificationHeterogenous() throws {
    shouldFail {
      verify(self.classMock.method(first: "a", second: any())).wasNeverCalled()
    }
    shouldFail {
      verify(self.classMock.method(first: any(), second: "b")).wasNeverCalled()
    }
  }
  
  
  // MARK: - Throwing
  
  func testThrowNSError() throws {
    given(try classMock.throwingMethod())
      .willThrow(NSError(domain: "co.bird.mockingbird.error", code: 1, userInfo: nil))
    XCTAssertThrowsError(try classMock.throwingMethod(), "Mock should throw", { error in
      XCTAssertEqual((error as NSError).domain, "co.bird.mockingbird.error")
      XCTAssertEqual((error as NSError).code, 1)
    })
    verify(try classMock.throwingMethod()).wasCalled()
  }
  
  func testThrowSwiftErrorStruct() throws {
    struct FakeError: LocalizedError {
      let errorDescription: String? = "foobar"
    }
    given(try classMock.throwingMethod()).willThrow(FakeError())
    XCTAssertThrowsError(try classMock.throwingMethod(), "Mock should throw", { error in
      XCTAssertEqual(error.localizedDescription, "foobar")
    })
    verify(try classMock.throwingMethod()).wasCalled()
  }
  
  func testThrowSwiftErrorClass() throws {
    class FakeError: LocalizedError {
      let errorDescription: String? = "foobar"
    }
    given(try classMock.throwingMethod()).willThrow(FakeError())
    XCTAssertThrowsError(try classMock.throwingMethod(), "Mock should throw", { error in
      XCTAssertEqual(error.localizedDescription, "foobar")
    })
    verify(try classMock.throwingMethod()).wasCalled()
  }
  
  func testThrowFromClosure() throws {
    struct FakeError: LocalizedError {
      let errorDescription: String? = "foobar"
    }
    given(try classMock.throwingMethod()).will { throw FakeError() }
    XCTAssertThrowsError(try classMock.throwingMethod(), "Mock should throw", { error in
      XCTAssertEqual(error.localizedDescription, "foobar")
    })
    verify(try classMock.throwingMethod()).wasCalled()
  }
  
  func testThrowingOnNonThrowingMethod() throws {
    struct FakeError: Error {}
    given(classMock.trivialMethod()).willThrow(FakeError())
    classMock.trivialMethod()
    verify(classMock.trivialMethod()).wasCalled()
  }
  
  func testSubclass() throws {
    let expectation = XCTestExpectation()
    expectation.expectedFulfillmentCount = 2
    given(subclassMock.method(valueType: any())).will { (val: Bool) in
      expectation.fulfill()
      return val
    }
    XCTAssertTrue(subclassMock.method(valueType: true))
    XCTAssertFalse(subclassMock.method(valueType: false))
    verify(subclassMock.method(valueType: true)).wasCalled()
    verify(subclassMock.method(valueType: false)).wasCalled()
    verify(subclassMock.method(valueType: any())).wasCalled(twice)
    wait(for: [expectation], timeout: 2)
  }
  
  
  // MARK: - Optionals
  
  func testMethodReturningOptionalNilValue() throws {
    given(classMock.methodReturningOptionalValue()).willReturn(nil)
    XCTAssertNil(classMock.methodReturningOptionalValue())
    verify(classMock.methodReturningOptionalValue()).wasCalled()
  }
  
  func testMethodReturningOptionalNonNilValue() throws {
    given(classMock.methodReturningOptionalValue()).willReturn("foobar")
    XCTAssertEqual(classMock.methodReturningOptionalValue(), "foobar")
    verify(classMock.methodReturningOptionalValue()).wasCalled()
  }
  
  func testOptionalPropertyWithNilValue() throws {
    given(classMock.optionalProperty).willReturn(nil)
    XCTAssertNil(classMock.optionalProperty)
    verify(classMock.optionalProperty).wasCalled()
  }
  
  func testOptionalPropertyWithNonNilValue() throws {
    given(classMock.optionalProperty).willReturn("foobar")
    XCTAssertEqual(classMock.optionalProperty, "foobar")
    verify(classMock.optionalProperty).wasCalled()
  }
}
