defmodule Doctor.ModuleReportTest do
  use ExUnit.Case

  alias Doctor.{ModuleInformation, ModuleReport}

  test "build/1 should build the correct report struct for a file with full coverage" do
    module_report =
      Doctor.AllDocs
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.AllDocs)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 7
    assert module_report.has_module_doc
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.AllDocs"
    assert module_report.doc_coverage == Decimal.new("100")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file with partial coverage" do
    module_report =
      Doctor.PartialDocs
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.PartialDocs)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 7
    refute module_report.has_module_doc
    assert module_report.missed_docs == 3
    assert module_report.missed_specs == 3
    assert module_report.module == "Doctor.PartialDocs"
    assert module_report.doc_coverage == Decimal.new("57.14285714285714285714285714")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file that implements behaviour callbacks" do
    module_report =
      Doctor.BehaviourModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.BehaviourModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 3
    assert module_report.has_module_doc
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.BehaviourModule"
    assert module_report.doc_coverage == Decimal.new("100")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file that implements behaviour callbacks with multiple clauses" do
    module_report =
      Doctor.FooBar
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.FooBar)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 6
    assert module_report.has_module_doc
    assert module_report.missed_docs == 1
    assert module_report.missed_specs == 3
    assert module_report.module == "Doctor.FooBar"
    assert module_report.doc_coverage == Decimal.new("83.33333333333333333333333333")
    assert module_report.spec_coverage == Decimal.new("50.0")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file with no coverage" do
    module_report =
      Doctor.NoDocs
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.NoDocs)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 7
    refute module_report.has_module_doc
    assert module_report.missed_docs == 7
    assert module_report.missed_specs == 7
    assert module_report.module == "Doctor.NoDocs"
    assert module_report.doc_coverage == Decimal.new("0")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file with struct specs" do
    module_report =
      Doctor.StructSpecModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.StructSpecModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 0
    refute module_report.has_module_doc
    assert module_report.has_struct_type_spec
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.StructSpecModule"
    assert module_report.doc_coverage == nil
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file with no struct specs" do
    module_report =
      Doctor.NoStructSpecModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.NoStructSpecModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 0
    refute module_report.has_module_doc
    refute module_report.has_struct_type_spec
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.NoStructSpecModule"
    assert module_report.doc_coverage == nil
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report struct for a file with an opaque struct spec" do
    module_report =
      Doctor.OpaqueStructSpecModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.OpaqueStructSpecModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 0
    refute module_report.has_module_doc
    assert module_report.has_struct_type_spec
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.OpaqueStructSpecModule"
    assert module_report.doc_coverage == nil
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report for an exception" do
    module_report =
      Doctor.Exception
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.Exception)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 1
    refute module_report.has_module_doc
    assert module_report.has_struct_type_spec
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.Exception"
    assert module_report.doc_coverage == Decimal.new("100")
    assert module_report.properties == [is_exception: true, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report for a module with __using__ macro" do
    module_report =
      Doctor.UseModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.UseModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 4
    assert module_report.has_module_doc
    assert module_report.has_struct_type_spec == :not_struct
    assert module_report.missed_docs == 2
    assert module_report.missed_specs == 2
    assert module_report.module == "Doctor.UseModule"
    assert module_report.doc_coverage == Decimal.new("50.0")
    assert module_report.spec_coverage == Decimal.new("50.0")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report for a module with a nested module" do
    module_report =
      Doctor.ParentModule
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.ParentModule)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 1
    assert module_report.has_module_doc
    assert module_report.has_struct_type_spec == :not_struct
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.ParentModule"
    assert module_report.doc_coverage == Decimal.new("100")
    assert module_report.spec_coverage == Decimal.new("100")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report for a nested module" do
    module_report =
      Doctor.ParentModule.Nested
      |> Code.fetch_docs()
      |> ModuleInformation.build(Doctor.ParentModule.Nested)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.functions == 1
    assert module_report.has_module_doc
    assert module_report.has_struct_type_spec == :not_struct
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Doctor.ParentModule.Nested"
    assert module_report.doc_coverage == Decimal.new("100")
    assert module_report.spec_coverage == Decimal.new("100")
    assert module_report.properties == [is_exception: false, is_protocol_implementation: false]
  end

  test "build/1 should build the correct report for a protocol derivation" do
    module_report =
      Inspect.Doctor.DeriveProtocol
      |> Code.fetch_docs()
      |> ModuleInformation.build(Inspect.Doctor.DeriveProtocol)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.is_protocol_implementation == true
    assert module_report.functions == 0
    assert module_report.has_module_doc == true
    assert module_report.has_struct_type_spec == :not_struct
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Inspect.Doctor.DeriveProtocol"
    assert module_report.doc_coverage == nil
    assert module_report.spec_coverage == nil
    assert module_report.properties == [is_exception: false, is_protocol_implementation: true]
  end

  test "build/1 should build the correct report for a protocol implementation" do
    module_report =
      Inspect.Doctor.ImplementProtocol
      |> Code.fetch_docs()
      |> ModuleInformation.build(Inspect.Doctor.ImplementProtocol)
      |> ModuleInformation.load_file_ast()
      |> ModuleInformation.load_user_defined_functions()
      |> ModuleReport.build()

    assert module_report.is_protocol_implementation == true
    assert module_report.functions == 0
    assert module_report.has_module_doc == true
    assert module_report.has_struct_type_spec == :not_struct
    assert module_report.missed_docs == 0
    assert module_report.missed_specs == 0
    assert module_report.module == "Inspect.Doctor.ImplementProtocol"
    assert module_report.doc_coverage == nil
    assert module_report.spec_coverage == nil
    assert module_report.properties == [is_exception: false, is_protocol_implementation: true]
  end
end
