// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2021 The Gleam contributors

use crate::javascript::tests::CURRENT_PACKAGE;
use crate::{assert_js, assert_ts_def};

#[test]
fn zero_arity_literal() {
    assert_js!(
        r#"
pub type Mine {
    This
    ThatOneIsAMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchLongerVariant
}

pub fn go() {
    This
    ThatOneIsAMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchLongerVariant
}
"#,
    );
}

#[test]
fn zero_arity_const() {
    assert_js!(
        r#"
pub type Mine {
    This
    ThatOneIsAMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchLongerVariant
}

pub const this = This
pub const that = ThatOneIsAMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchMuchLongerVariant
"#,
    );
}

#[test]
fn zero_arity_imported() {
    assert_js!(
        ("other", r#"pub type One { Two }"#),
        r#"import other
pub fn main() {
  other.Two
}"#,
    );
}

#[test]
fn zero_arity_imported_typscript() {
    assert_ts_def!(
        (CURRENT_PACKAGE, "other", r#"pub type One { Two }"#),
        r#"import other
pub fn main() {
  other.Two
}"#,
    );
}

#[test]
fn zero_arity_imported_unqualified() {
    assert_js!(
        ("other", r#"pub type One { Two }"#),
        r#"import other.{Two}
pub fn main() {
  Two
}"#,
    );
}

#[test]
fn zero_arity_imported_unqualified_typescript() {
    assert_ts_def!(
        (CURRENT_PACKAGE, "other", r#"pub type One { Two }"#),
        r#"import other.{Two}
pub fn main() {
  Two
}"#,
    );
}

#[test]
fn zero_arity_imported_unqualified_aliased() {
    assert_js!(
        ("other", r#"pub type One { Two }"#),
        r#"import other.{Two as Three}
pub fn main() {
  Three
}"#
    );
}

#[test]
fn zero_arity_imported_unqualified_aliased_typescript() {
    assert_ts_def!(
        (CURRENT_PACKAGE, "other", r#"pub type One { Two }"#),
        r#"import other.{Two as Three}
pub fn main() {
  Three
}"#
    );
}

#[test]
fn const_zero_arity_imported() {
    assert_js!(
        ("other", r#"pub type One { Two }"#),
        r#"import other
pub const x = other.Two
"#,
    );
}

#[test]
fn const_zero_arity_imported_unqualified() {
    assert_js!(
        ("other", r#"pub type One { Two }"#),
        r#"import other.{Two}
pub const a = Two
"#,
    );
}

#[test]
fn const_with_fields() {
    assert_js!(
        r#"
pub type Mine {
  Mine(a: Int, b: Int)
}

pub const labels = Mine(b: 2, a: 1)
pub const no_labels = Mine(3, 4)
"#,
    );
}

#[test]
fn const_with_fields_typescript() {
    assert_ts_def!(
        r#"
pub type Mine {
  Mine(a: Int, b: Int)
}

pub const labels = Mine(b: 2, a: 1)
pub const no_labels = Mine(3, 4)
"#,
    );
}

#[test]
fn unnamed_fields() {
    assert_js!(
        r#"
pub type Ip {
    Ip(String)
}

pub const local = Ip("0.0.0.0")

pub fn build(x) {
    x("1.2.3.4")
}

pub fn go() {
    build(Ip)
    Ip("5.6.7.8")
}

pub fn destructure(x) {
  let Ip(raw) = x
  raw
}
"#,
    );
}

#[test]
fn unnamed_fields_typescript() {
    assert_ts_def!(
        r#"
pub type Ip{
    Ip(String)
}

pub const local = Ip("0.0.0.0")

"#,
    );
}

#[test]
fn long_name_variant_without_labels() {
    assert_js!(
        r#"
pub type TypeWithALongNameAndSeveralArguments{
  TypeWithALongNameAndSeveralArguments(String, String, String, String, String)
}


pub fn go() {
  TypeWithALongNameAndSeveralArguments
}
"#,
    );
}

#[test]
fn long_name_variant_mixed_labels_typescript() {
    assert_ts_def!(
        r#"
pub type TypeWithALongNameAndSeveralArguments{
  TypeWithALongNameAndSeveralArguments(String, String, String, a: String, b: String)
}

pub const local = TypeWithALongNameAndSeveralArguments("one", "two", "three", "four", "five")
"#,
    );
}

#[test]
fn custom_type_with_named_fields() {
    assert_js!(
        r#"
pub type Cat {
  Cat(name: String, cuteness: Int)
}

pub type Box {
  Box(occupant: Cat)
}

pub const felix = Cat("Felix", 12)
pub const tom = Cat(cuteness: 1, name: "Tom")

pub fn go() {
  Cat("Nubi", 1)
  Cat(2, name: "Nubi")
  Cat(cuteness: 3, name: "Nubi")
}

pub fn update(cat) {
  Cat(..cat, name: "Sid")
  Cat(..cat, name: "Bartholemew Wonder Puss the Fourth !!!!!!!!!!!!!!!!")
  Cat(..new_cat(), name: "Molly")
  let box = Box(occupant: cat)
  Cat(..box.occupant, cuteness: box.occupant.cuteness + 1)
}

pub fn access(cat: Cat) {
  cat.cuteness
}

pub fn new_cat() {
  Cat(name: "Beau", cuteness: 11)
}
"#,
    );
}

#[test]
fn destructure_custom_type_with_named_fields() {
    assert_js!(
        r#"
pub type Cat {
  Cat(name: String, cuteness: Int)
}

pub fn go(cat) {
  let Cat(x, y) = cat
  let Cat(name: x, ..) = cat
  let assert Cat(cuteness: 4, name: x) = cat
  x
}

"#,
    )
}

#[test]
fn destructure_custom_type_with_mixed_fields_first_unlabelled() {
    assert_js!(
        r#"
pub type Cat {
  Cat(String, cuteness: Int)
}

pub fn go(cat) {
  let Cat(x, y) = cat
  let Cat(cuteness: y, ..) = cat
  let Cat(x, cuteness: y) = cat
  x
}

"#,
    )
}

#[test]
fn nested_pattern_with_labels() {
    assert_js!(
        r#"pub type Box(x) { Box(a: Int, b: x) }
pub fn go(x) {
  case x {
    Box(a: _, b: Box(a: a, b: b)) -> a + b
    _ -> 1
  }
}
"#,
    );
}

#[test]
fn imported_no_label() {
    assert_js!(
        ("other", r#"pub type One { Two(Int) }"#),
        r#"import other
pub fn main() {
  other.Two(1)
}"#,
    );
}

#[test]
fn imported_ignoring_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other
pub fn main() {
  other.Two(1)
}"#,
    );
}

#[test]
fn imported_using_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other
pub fn main() {
  other.Two(field: 1)
}"#,
    );
}

#[test]
fn imported_multiple_fields() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other
pub fn main() {
  other.Two(b: 2, c: 3, a: 1)
}"#,
    );
}

#[test]
fn unqualified_imported_no_label() {
    assert_js!(
        ("other", r#"pub type One { Two(Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two(1)
}"#,
    );
}

#[test]
fn unqualified_imported_no_label_typescript() {
    assert_ts_def!(
        (CURRENT_PACKAGE, "other", r#"pub type One { Two(Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two(1)
}"#,
    );
}

#[test]
fn unqualified_imported_ignoring_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two(1)
}"#,
    );
}

#[test]
fn unqualified_imported_using_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two(field: 1)
}"#,
    );
}

#[test]
fn unqualified_imported_multiple_fields() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two(b: 2, c: 3, a: 1)
}"#,
    );
}

#[test]
fn constructor_as_value() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other
pub fn main() {
  other.Two
}"#,
    );
}

#[test]
fn unqualified_constructor_as_value() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other.{Two}
pub fn main() {
  Two
}"#,
    );
}

#[test]
fn const_imported_no_label() {
    assert_js!(
        ("other", r#"pub type One { Two(Int) }"#),
        r#"import other
pub const main = other.Two(1)
"#,
    );
}

#[test]
fn const_imported_ignoring_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other
pub const main = other.Two(1)
"#,
    );
}

#[test]
fn const_imported_using_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other
pub const main = other.Two(field: 1)
"#,
    );
}

#[test]
fn const_imported_multiple_fields() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other
pub const main = other.Two(b: 2, c: 3, a: 1)
"#,
    );
}

#[test]
fn const_unqualified_imported_no_label() {
    assert_js!(
        ("other", r#"pub type One { Two(Int) }"#),
        r#"import other.{Two}
pub const main = Two(1)
"#,
    );
}

#[test]
fn const_unqualified_imported_ignoring_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other.{Two}
pub const main = Two(1)
"#,
    );
}

#[test]
fn const_unqualified_imported_using_label() {
    assert_js!(
        ("other", r#"pub type One { Two(field: Int) }"#),
        r#"import other.{Two}
pub const main = Two(field: 1)
"#,
    );
}

#[test]
fn const_unqualified_imported_multiple_fields() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other.{Two}
pub const main = Two(b: 2, c: 3, a: 1)
"#,
    );
}

#[test]
fn imported_pattern() {
    assert_js!(
        ("other", r#"pub type One { Two(a: Int, b: Int, c: Int) }"#),
        r#"import other.{Two}

pub fn main(x) {
  case x {
    Two(a: 1, ..) -> 1
    other.Two(b: 2, c: c, ..) -> c
    _ -> 3
  }
}
"#,
    );
}

#[test]
fn keyword_label_name() {
    assert_js!(
        r#"pub type Thing {
  Thing(in: Int, class: Nil)
}
"#,
    );
}

#[test]
fn qualified() {
    assert_js!(
        ("other", r#"pub type One { One }"#),
        r#"import other

pub fn main() {
  other.One
}
"#,
    );
}

#[test]
fn unapplied_record_constructors_typescript() {
    assert_ts_def!(
        r#"pub type Cat { Cat(name: String) }

pub fn return_unapplied_cat() {
  Cat
}
"#
    );
}

#[test]
fn opaque_types_typescript() {
    assert_ts_def!(
        r#"pub opaque type Animal {
  Cat(goes_outside: Bool)
  Dog(plays_fetch: Bool)
}
"#
    );
}

// https://github.com/gleam-lang/gleam/issues/1650
#[test]
fn types_must_be_rendered_before_functions() {
    assert_js!(
        r#"
pub fn one() { One }
pub type One { One }
"#
    );
}

// https://github.com/gleam-lang/gleam/issues/2386
#[test]
fn new_type_import_syntax() {
    assert_js!(
        ("package", "a", r#"pub type A { A }"#),
        r#"
import a.{type A, A}

pub fn main() {
  A
}
"#
    );
}

// https://github.com/gleam-lang/gleam/issues/3813
#[test]
fn record_with_field_named_constructor() {
    assert_js!(
        r#"
pub type Thing {
  Thing(constructor: Nil)
}

pub fn main() {
  let a = Thing(constructor: Nil)
  let b = Thing(..a, constructor: Nil)
  b.constructor
}
"#
    );
}

#[test]
fn record_with_field_named_then() {
    assert_js!(
        r#"
pub type Thing {
  Thing(then: Nil)
}

pub fn main() {
  let a = Thing(then: Nil)
  let b = Thing(..a, then: Nil)
  b.then
}
"#
    );
}
#[test]
fn record_access_in_guard_with_reserved_field_name() {
    assert_js!(
        r#"
pub type Thing {
  Thing(constructor: Nil)
}

pub fn main() {
  let a = Thing(constructor: Nil)
  case Nil {
      Nil if a.constructor == Nil -> a.constructor
      _ -> Nil
  }
}
"#
    );
}

#[test]
fn record_access_in_pattern_with_reserved_field_name() {
    assert_js!(
        r#"
pub type Thing {
  Thing(constructor: Nil)
}

pub fn main() {
  let a = Thing(constructor: Nil)
  let Thing(constructor: ctor) = a
  case a {
      a if a.constructor == ctor -> Nil
      Thing(constructor:) if ctor == constructor -> Nil
      _ -> Nil
  }
}
"#
    );
}

#[test]
fn constructors_get_their_own_jsdoc() {
    assert_js!(
        r#"
pub type Wibble {
  /// Wibbling!!
  Wibble(field: Int)

  /// Wobbling!!
  Wobble(field: Int)
}
"#
    );
}

#[test]
fn singleton_record_equality() {
    assert_js!(
        r#"
pub type Wibble {
  Wibble
  Wobble
}

pub fn is_wibble(w: Wibble) -> Bool {
  w == Wibble
}
"#,
    );
}

#[test]
fn singleton_record_inequality() {
    assert_js!(
        r#"
pub type Wibble {
  Wibble
  Wobble
}

pub fn is_not_wibble(w: Wibble) -> Bool {
  w != Wibble
}
"#,
    );
}

#[test]
fn singleton_record_reverse_order() {
    assert_js!(
        r#"
pub type Wibble {
  Wibble
  Wobble
}

pub fn is_wibble_reverse(w: Wibble) -> Bool {
  Wibble == w
}
"#,
    );
}

#[test]
fn non_singleton_record_equality() {
    assert_js!(
        r#"
pub type Person {
  Person(name: String, age: Int)
}

pub fn same_person(p1: Person, p2: Person) -> Bool {
  p1 == p2
}
"#,
    );
}

#[test]
fn multiple_singleton_constructors() {
    assert_js!(
        r#"
pub type Status {
  Loading
  Success
  Error
}

pub fn is_loading(s: Status) -> Bool {
  s == Loading
}

pub fn is_success(s: Status) -> Bool {
  s == Success
}
"#,
    );
}

#[test]
fn mixed_singleton_and_non_singleton() {
    assert_js!(
        r#"
pub type Result {
  Ok(value: Int)
  Error
}

pub fn is_error(r: Result) -> Bool {
  r == Error
}
"#,
    );
}

#[test]
fn singleton_in_case_guard() {
    assert_js!(
        r#"
pub type State {
  Active
  Inactive
}

pub fn process(s: State) -> String {
  case s {
    state if state == Active -> "active"
    _ -> "inactive"
  }
}
"#,
    );
}

#[test]
fn equality_with_non_singleton_variant() {
    assert_js!(
        r#"
pub type Thing {
  Variant
  Other(String)
}

pub fn check_other(x: Thing) -> Bool {
  x == Other("hello")
}
"#,
    );
}

#[test]
fn guard_equality_with_non_singleton_variant() {
    assert_js!(
        r#"
pub type Thing {
  Variant
  Other(String)
}

pub fn process(e: Thing) -> String {
  case e {
    value if value == Other("hello") -> "match"
    _ -> "no match"
  }
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_qualified_expression() {
    assert_js!(
        (
            "other_module",
            r#"pub type Thingy { Variant OtherVariant }"#
        ),
        r#"
import other_module

pub fn check(x) -> Bool {
  x == other_module.Variant
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_unqualified_expression() {
    assert_js!(
        ("other_module", r#"pub type Thingy { Variant Other(Int) }"#),
        r#"
import other_module.{Variant}

pub fn check(x) -> Bool {
  x == Variant
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_aliased_expression() {
    assert_js!(
        ("other_module", r#"pub type Thingy { Variant Other(Int) }"#),
        r#"
import other_module.{Variant as Aliased}

pub fn check(x) -> Bool {
  x == Aliased
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_qualified_clause_guard() {
    assert_js!(
        ("other_module", r#"pub type Thingy { Variant Other(Int) }"#),
        r#"
import other_module

pub fn process(e) -> String {
  case e {
    value if value == other_module.Variant -> "match"
    _ -> "no match"
  }
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_unqualified_clause_guard() {
    assert_js!(
        ("other_module", r#"pub type Thingy { Variant Other(Int) }"#),
        r#"
import other_module.{Variant}

pub fn process(e) -> String {
  case e {
    value if value == Variant -> "match"
    _ -> "no match"
  }
}
"#,
    );
}

#[test]
fn variant_defined_in_another_module_aliased_clause_guard() {
    assert_js!(
        ("other_module", r#"pub type Thingy { Variant Other(Int) }"#),
        r#"
import other_module.{Variant as Aliased}

pub fn process(e) -> String {
  case e {
    value if value == Aliased -> "match"
    _ -> "no match"
  }
}
"#,
    );
}

#[test]
fn external_annotation() {
    assert_ts_def!(
        r#"
@external(javascript, "./gleam_stdlib.d.ts", "Dict")
pub type Dict(key, value)
"#
    );
}

#[test]
fn external_annotated_type_used_in_function() {
    assert_ts_def!(
        r#"
@external(javascript, "./gleam_stdlib.d.ts", "Dict")
pub type Dict(key, value)

@external(javascript, "./gleam_stdlib.mjs", "get")
pub fn get(dict: Dict(key, value), key: key) -> Result(value, Nil)
"#
    );
}

// https://github.com/gleam-lang/gleam/issues/5127
#[test]
fn unused_opaque_constructor_is_generated_correctly() {
    assert_ts_def!(
        "
type Wibble {
  Wibble
}

pub opaque type Wobble {
  Wobble(Wibble)
}
"
    );
}

// https://github.com/gleam-lang/gleam/issues/5312
#[test]
fn generic_type_parameter_used_in_field() {
    assert_ts_def!(
        "
pub type Wibble(value, error) {
  Wibble(
    wibble: value,
    wobble: Result(value, error),
    wubble: error,
  )
}
"
    );
}

// https://github.com/gleam-lang/gleam/issues/5757
#[test]
fn zero_arity_type_with_import_alias() {
    assert_js!(
        (
            "wibble",
            r#"
pub type WibbleStatus {
  Active
  Inactive
}
"#
        ),
        r#"
import wibble.{type WibbleStatus, Active as WibbleActive}

pub fn is_wibble_active(s: WibbleStatus) -> Bool {
  s == WibbleActive
}
"#,
    );
}

// https://github.com/gleam-lang/gleam/issues/5757
#[test]
fn zero_arity_type_with_import_alias_and_same_named_local_type() {
    assert_js!(
        (
            "wibble",
            r#"
pub type WibbleStatus {
  Active
  Inactive
}
"#
        ),
        r#"
import wibble.{type WibbleStatus, Active as WibbleActive}

pub type WobbleStatus {
  Active
  Disabled
}

pub fn is_wibble_active(s: WibbleStatus) -> Bool {
  s == WibbleActive
}

pub fn is_wobble_active(s: WobbleStatus) -> Bool {
  s == Active
}
"#,
    );
}

// https://github.com/gleam-lang/gleam/issues/5613
#[test]
pub fn type_with_constructor_as_field_is_properly_escaped() {
    assert_js!(
        "
pub type Box {
  Box(constructor: Int)
}
"
    )
}

#[test]
fn type_with_constructor_as_field_is_properly_escaped_in_ts_def() {
    assert_ts_def!(
        r#"
pub type Box {
  Box(constructor: Int)
}
"#,
    );
}

#[test]
fn singleton_for_two_aliased_variants() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble }"),
        ("wobble", "pub type Wibble { Wibble }"),
        "
import wobble.{Wibble as Wobble}
import wibble.{Wibble as Wubble}

pub fn main() {
  #(Wobble, Wubble)
}
"
    );
}

#[test]
fn fieldless_variant_still_imported_if_used_in_instanceof_comparison() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wibble}

pub fn main() {
  let x = Wibble
  x == Wibble
}
"
    );
}

#[test]
fn fieldless_variant_still_imported_if_used_in_instanceof_comparison_in_guard() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wibble}

pub fn main() {
  let x = Wibble
  case x {
    _ if x == Wibble -> 1
    _ -> 2
  }
}
"
    );
}

#[test]
fn aliased_variant_still_imported_if_used_in_instanceof_comparison() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wibble as Wobble}

pub fn main() {
  let x = Wobble
  x == Wobble
}
"
    );
}

#[test]
fn aliased_variant_still_imported_if_used_in_instanceof_comparison_in_guard() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wibble as Wobble}

pub fn go(x) {
  case x {
    _ if x == Wobble -> Wobble
    _ -> wibble.Wobble
  }
}
"
    );
}

#[test]
fn aliased_variant_still_imported_if_used_in_pattern_matching() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wibble as Wobble}

pub fn go(x) {
  case x {
    Wobble -> wibble.Wobble
    _ -> Wobble
  }
}
"
    );
}

#[test]
fn opaque_variant_produces_private_singleton_constants() {
    assert_js!(
        "
pub opaque type Wibble {
  Wibble
  Wobble(Int)
}

pub fn wibble() -> Wibble {
  Wibble
}

pub fn wobble(x: Int) -> Wibble {
  Wobble(x)
}
"
    );
}

#[test]
fn singleton_constant_used_in_const() {
    assert_js!(
        "
pub opaque type Wibble {
  Wibble
  Wobble(Int)
}

pub const wibble = Wibble
"
    );
}

#[test]
fn singleton_constant_from_other_module_used_in_const() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wobble}

pub const wibble = wibble.Wibble

pub const wobble = Wobble
"
    );
}

#[test]
fn singleton_constant_used_in_guard() {
    assert_js!(
        "
pub opaque type Wibble {
  Wibble
  Wobble(Int)
}

pub fn go(x) {
  case x {
    _ if x == #(Wibble) -> True
    _ -> False
  }
}
"
    );
}

#[test]
fn singleton_constant_from_other_module_used_in_guard() {
    assert_js!(
        ("wibble", "pub type Wibble { Wibble Wobble }"),
        "
import wibble.{Wobble}

pub fn go(x) {
  case x {
    _ if x == #(wibble.Wibble) -> 1
    _ if x == #(Wobble) -> 2
    _ -> 3
  }
}
"
    );
}
