// REQUIRES: swift_swift_parser, executable_test

// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature Macros -Xfrontend -plugin-path -Xfrontend %swift-plugin-dir)

// Run this test via the swift-plugin-server
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature Macros -Xfrontend -external-plugin-path -Xfrontend %swift-plugin-dir#%swift-plugin-server)

// REQUIRES: observation
// REQUIRES: concurrency
// REQUIRES: objc_interop
// REQUIRES: swift_feature_Macros
// UNSUPPORTED: use_os_stdlib
// UNSUPPORTED: back_deployment_runtime

import StdlibUnittest
import Observation

@usableFromInline
@inline(never)
func _blackHole<T>(_ value: T) { }

@Observable
class ContainsNothing { }

@Observable
class ContainsWeak {
  weak var obj: AnyObject? = nil
}

@Observable
public class PublicContainsWeak {
  public weak var obj: AnyObject? = nil
}

@Observable
class ContainsUnowned {
  unowned var obj: AnyObject? = nil
}

@Observable
class ContainsIUO {
  var obj: Int! = nil
}

class NonObservable {

}

@Observable
class InheritsFromNonObservable: NonObservable {

}

protocol NonObservableProtocol {

}

@Observable
class ConformsToNonObservableProtocol: NonObservableProtocol {

}

struct NonObservableContainer {
  @Observable
  class ObservableContents {
    var field: Int = 3
  }
}

@Observable
final class SendableClass: Sendable {
  var field: Int = 3
}

@Observable
class CodableClass: Codable {
  var field: Int = 3
}

@Observable
final class HashableClass {
  var field: Int = 3
}

extension HashableClass: Hashable {
  static func == (lhs: HashableClass, rhs: HashableClass) -> Bool {
    lhs.field == rhs.field
  }

  func hash(into hasher: inout Hasher) {
    hasher.combine(field)
  }
}

@Observable
class ImplementsAccessAndMutation {
  var field = 3
  let accessCalled: (PartialKeyPath<ImplementsAccessAndMutation>) -> Void
  let withMutationCalled: (PartialKeyPath<ImplementsAccessAndMutation>) -> Void

  init(accessCalled: @escaping (PartialKeyPath<ImplementsAccessAndMutation>) -> Void, withMutationCalled: @escaping (PartialKeyPath<ImplementsAccessAndMutation>) -> Void) {
    self.accessCalled = accessCalled
    self.withMutationCalled = withMutationCalled
  }

  internal func access<Member>(
      keyPath: KeyPath<ImplementsAccessAndMutation , Member>
  ) {
    accessCalled(keyPath)
    _$observationRegistrar.access(self, keyPath: keyPath)
  }

  internal func withMutation<Member, T>(
    keyPath: KeyPath<ImplementsAccessAndMutation , Member>,
    _ mutation: () throws -> T
  ) rethrows -> T {
    withMutationCalled(keyPath)
    return try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation)
  }
}

@Observable
class HasIgnoredProperty {
  var field = 3
  @ObservationIgnored var ignored = 4
}

@Observable
class Entity {
  var age: Int = 0
}

@Observable
class Person : Entity {
  var firstName = ""
  var lastName = ""

  var friends = [Person]()

  var fullName: String { firstName + " " + lastName }
}

@Observable
class MiddleNamePerson: Person {
  var middleName = ""

  override var fullName: String { firstName + " " + middleName + " " + lastName }
}

@Observable
class IsolatedClass {
  @MainActor var test = "hello"
}

@MainActor
@Observable
class IsolatedInstance {
  var test = "hello"
}

@Observable
class IgnoredComputed {
  @ObservationIgnored
  var message: String { "hello" }
}

@Observable
class ClassHasExistingConformance: Observable { }

protocol Intermediary: Observable { }

@Observable
class HasIntermediaryConformance: Intermediary { }

class CapturedState<State>: @unchecked Sendable {
  var state: State

  init(state: State) {
    self.state = state
  }
}

@Observable
class RecursiveInner {
  var value = "prefix"
}

@Observable
class GenericClass<T> {
  var value = 3
}

struct StructParent {
  @Observable
  class NestedGenericClass<T> {
    var value = 3
  }
}

struct GenericStructParent<T> {
  @Observable
  class NestedClass {
    var value = 3
  }
}

class ClassParent {
  @Observable
  class NestedGenericClass<T> {
    var value = 3
  }
}

class GenericClassParent<T> {
  @Observable
  class NestedClass {
    var value = 3
  }
}

@Observable
class RecursiveOuter {
  var inner = RecursiveInner()
  var value = "prefix"
  @ObservationIgnored var innerEventCount = 0
  @ObservationIgnored var outerEventCount = 0

  func recursiveTrackingCalls() {
    withObservationTracking({
      let _ = value
      _ = withObservationTracking({
        inner.value
      }, onChange: {
        self.innerEventCount += 1
      })
    }, onChange: {
      self.outerEventCount += 1
    })
  }
}

@Observable
#if FOO
@available(SwiftStdlib 5.9, *)
#elseif BAR
@available(SwiftStdlib 5.9, *)
#else
#endif
class GuardedAvailability {
}

@Observable class TestASTScopeLCA {
  // Make sure ASTScope unqualified lookup can find local variables
  // inside initial values with closures when accessor macros are
  // involved.
  var state : Bool = {
    let value = true
    return value
  }()
}

@Observable class Parent {
  class Nested {}
}

extension Parent.Nested {}

struct CowContainer {
  final class Contents { }
  
  var contents = Contents()
  
  mutating func mutate() {
    if !isKnownUniquelyReferenced(&contents) {
      contents = Contents()
    }
  }
  
  var id: ObjectIdentifier {
    ObjectIdentifier(contents)
  }
}


@Observable
final class CowTest {
  var container = CowContainer()
}

@main
struct Validator {
  @MainActor
  static func main() async {

    
    let suite = TestSuite("Observable")

    suite.test("only instantiate") {
      let test = MiddleNamePerson()
    }
    
    suite.test("unobserved value changes") {
      let test = MiddleNamePerson()
      for i in 0..<100 {
        test.firstName = "\(i)"
      }
    }
    
    suite.test("tracking changes") {
      let changed = CapturedState(state: false)
      
      let test = MiddleNamePerson()
      withObservationTracking {
        _blackHole(test.firstName)
      } onChange: {
        changed.state = true
      }
      
      test.firstName = "c"
      expectEqual(changed.state, true)
      changed.state = false
      test.firstName = "c"
      expectEqual(changed.state, false)
    }

    suite.test("conformance") {
      func testConformance<O: Observable>(_ o: O) -> Bool {
        return true
      }
      
      func testConformance<O>(_ o: O) -> Bool {
        return false
      }
      
      let test = Person()
      expectEqual(testConformance(test), true)
    }
    
    suite.test("tracking nonchanged") {
      let changed = CapturedState(state: false)
      
      let test = MiddleNamePerson()
      withObservationTracking {
        _blackHole(test.lastName)
      } onChange: {
        changed.state = true
      }
      
      test.firstName = "c"
      expectEqual(changed.state, false)
    }
    
    suite.test("tracking computed") {
      let changed = CapturedState(state: false)
      
      let test = MiddleNamePerson()
      withObservationTracking {
        _blackHole(test.fullName)
      } onChange: {
        changed.state = true
      }
      
      test.middleName = "c"
      expectEqual(changed.state, true)
      changed.state = false
      test.middleName = "c"
      expectEqual(changed.state, false)
    }
    
    suite.test("graph changes") {
      let changed = CapturedState(state: false)
      
      let test = MiddleNamePerson()
      let friend = MiddleNamePerson()
      test.friends.append(friend)
      withObservationTracking {
        _blackHole(test.friends.first?.fullName)
      } onChange: {
        changed.state = true
      }

      test.middleName = "c"
      expectEqual(changed.state, false)
      friend.middleName = "c"
      expectEqual(changed.state, true)
    }
    
    suite.test("nesting") {
      let changedOuter = CapturedState(state: false)
      let changedInner = CapturedState(state: false)
      
      let test = MiddleNamePerson()
      withObservationTracking {
        withObservationTracking {
          _blackHole(test.firstName)
        } onChange: {
          changedInner.state = true
        }
      } onChange: {
        changedOuter.state = true
      }
      
      test.firstName = "c"
      expectEqual(changedInner.state, true)
      expectEqual(changedOuter.state, true)
      changedOuter.state = false
      test.firstName = "c"
      expectEqual(changedOuter.state, false)
    }
    
    suite.test("access and mutation") {
      let accessKeyPath = CapturedState<PartialKeyPath<ImplementsAccessAndMutation>?>(state: nil)
      let mutationKeyPath = CapturedState<PartialKeyPath<ImplementsAccessAndMutation>?>(state: nil)
      let test = ImplementsAccessAndMutation { keyPath in
        accessKeyPath.state = keyPath
      } withMutationCalled: { keyPath in
        mutationKeyPath.state = keyPath
      }
      
      expectEqual(accessKeyPath.state, nil)
      _blackHole(test.field)
      expectEqual(accessKeyPath.state, \.field)
      expectEqual(mutationKeyPath.state, nil)
      accessKeyPath.state = nil
      test.field = 123
      expectEqual(accessKeyPath.state, nil)
      expectEqual(mutationKeyPath.state, \.field)
    }
    
    suite.test("ignores no change") {
      let changed = CapturedState(state: false)
      
      let test = HasIgnoredProperty()
      withObservationTracking {
        _blackHole(test.ignored)
      } onChange: {
        changed.state = true
      }
      
      test.ignored = 122112
      expectEqual(changed.state, false)
      changed.state = false
      test.field = 3429
      expectEqual(changed.state, false)
    }
    
    suite.test("ignores change") {
      let changed = CapturedState(state: false)
      
      let test = HasIgnoredProperty()
      withObservationTracking {
        _blackHole(test.ignored)
        _blackHole(test.field)
      } onChange: {
        changed.state = true
      }
      
      test.ignored = 122112
      expectEqual(changed.state, false)
      changed.state = false
      test.field = 3429
      expectEqual(changed.state, true)
    }

    suite.test("isolated class") { @MainActor in
      let changed = CapturedState(state: false)
      
      let test = IsolatedClass()
      withObservationTracking {
        _blackHole(test.test)
      } onChange: {
        changed.state = true
      }
      
      test.test = "c"
      expectEqual(changed.state, true)
      changed.state = false
      test.test = "c"
      expectEqual(changed.state, false)
    }

    suite.test("recursive tracking inner then outer") {
      let obj = RecursiveOuter()
      obj.recursiveTrackingCalls()
      obj.inner.value = "test"
      expectEqual(obj.innerEventCount, 1)
      expectEqual(obj.outerEventCount, 1)
      obj.recursiveTrackingCalls()
      obj.value = "test"
      expectEqual(obj.innerEventCount, 1)
      expectEqual(obj.outerEventCount, 2)
    }

    suite.test("recursive tracking outer then inner") {
      let obj = RecursiveOuter()
      obj.recursiveTrackingCalls()
      obj.value = "test"
      expectEqual(obj.innerEventCount, 0)
      expectEqual(obj.outerEventCount, 1)
      obj.recursiveTrackingCalls()
      obj.inner.value = "test"
      expectEqual(obj.innerEventCount, 2)
      expectEqual(obj.outerEventCount, 2)
    }
    
    suite.test("validate copy on write semantics") {
      let subject = CowTest()
      let startId = subject.container.id
      expectEqual(subject.container.id, startId)
      subject.container.mutate()
      expectEqual(subject.container.id, startId)
    }

    suite.test("tracking changes willSet option") {
      let changed = CapturedState(state: false)

      let test = MiddleNamePerson()
      withObservationTracking(options: .willSet) {
        _blackHole(test.firstName)
      } onChange: { _ in
        changed.state = true
      }

      test.firstName = "c"
      expectEqual(changed.state, true)
      changed.state = false
      test.firstName = "c"
      expectEqual(changed.state, false)
    }

    suite.test("tracking changes didSet option") {
      let changed = CapturedState(state: false)

      let test = MiddleNamePerson()
      withObservationTracking(options: .didSet) {
        _blackHole(test.firstName)
      } onChange: { _ in
        changed.state = true
      }

      test.firstName = "c"
      expectEqual(changed.state, true)
      changed.state = false
      test.firstName = "c"
      expectEqual(changed.state, false)
    }

    suite.test("tracking changes willSet & didSet option") {
        let changed = CapturedState(state: 0)

        let test = MiddleNamePerson()
        withObservationTracking(options: [.willSet, .didSet]) {
          _blackHole(test.firstName)
        } onChange: { _ in
          changed.state += 1
        }

        test.firstName = "c"
        expectEqual(changed.state, 2) // an event is triggered for both the willSet and the didSet
        changed.state = 0
        test.firstName = "c"
        expectEqual(changed.state, 0)
    }

    suite.test("tracking changes deinit option") {
        let changed = CapturedState(state: false)

        var test: MiddleNamePerson? = MiddleNamePerson()
        withObservationTracking(options: .deinit) {
          _blackHole(test?.firstName)
        } onChange: { _ in
          changed.state = true
        }

        test?.firstName = "c"
        expectEqual(changed.state, false)
        changed.state = false
        test?.firstName = "c"
        expectEqual(changed.state, false)
        test = nil
        expectEqual(changed.state, true)
    }

    suite.test("continuous tracking") {
      let changed = CapturedState(state: 0)
      let test: MiddleNamePerson = MiddleNamePerson()
      let token = withContinuousObservation(options: .didSet) { _ in
        _blackHole(test.firstName)
        changed.state += 1
      }
      // the sleeps here are intended to be a simulation of returning to the main run loop
      try! await Task.sleep(for: .seconds(0.1))
      expectEqual(changed.state, 1)
      test.firstName = "c"
      try! await Task.sleep(for: .seconds(0.1))
      expectEqual(changed.state, 2)
      test.firstName = "d"
      try! await Task.sleep(for: .seconds(0.1))
      expectEqual(changed.state, 3)
      token.cancel()
      try! await Task.sleep(for: .seconds(0.1)) // ensure a small grace w.r.t. cancellation
      test.firstName = "e"
      try! await Task.sleep(for: .seconds(0.1))
      expectEqual(changed.state, 3)
    }

    await runAllTestsAsync()
  }
}


