library(testthat)
library(DataAudit)


make_rule_data <- function() {

  data.frame(
    Animal_ID = c(
      "A001",
      "A002",
      "A002",
      NA,
      "BAD"
    ),
    Species = c(
      "Dog",
      "Cat",
      "Horse",
      " ",
      "Cattle"
    ),
    Age = c(
      5,
      3,
      40,
      NA,
      -2
    ),
    stringsAsFactors = FALSE
  )
}


test_that("apply_audit_rules returns correct class", {

  dat <- make_rule_data()

  result <- apply_audit_rules(
    dat,
    audit_rules()
  )

  expect_s3_class(
    result,
    "DataAuditRuleResult"
  )

  expect_type(
    result,
    "list"
  )
})


test_that("result contains expected components", {

  result <- apply_audit_rules(
    make_rule_data(),
    audit_rules()
  )

  expect_equal(
    names(result),
    c(
      "status",
      "total_violations",
      "summary",
      "range",
      "category",
      "unique",
      "required",
      "pattern"
    )
  )
})


test_that("empty rules pass", {

  result <- apply_audit_rules(
    make_rule_data(),
    audit_rules()
  )

  expect_equal(
    result$status,
    "Passed"
  )

  expect_equal(
    result$total_violations,
    0L
  )

  expect_equal(
    sum(result$summary$Violations),
    0
  )
})


test_that("range rules detect violations", {

  rules <- audit_rules(
    range = list(
      Age = c(0, 30)
    )
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$range$Age$Violation,
    c(
      FALSE,
      FALSE,
      TRUE,
      FALSE,
      TRUE
    )
  )

  expect_equal(
    sum(result$range$Age$Violation),
    2
  )
})


test_that("range rule rejects nonnumeric variable", {

  rules <- audit_rules(
    range = list(
      Species = c(0, 10)
    )
  )

  expect_error(
    apply_audit_rules(
      make_rule_data(),
      rules
    ),
    "Range rule requires 'Species' to be numeric"
  )
})


test_that("category rules detect violations", {

  rules <- audit_rules(
    category = list(
      Species = c(
        "Dog",
        "Cat",
        "Cattle"
      )
    )
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$category$Species$Violation,
    c(
      FALSE,
      FALSE,
      TRUE,
      TRUE,
      FALSE
    )
  )
})


test_that("unique rules detect all duplicated values", {

  rules <- audit_rules(
    unique = "Animal_ID"
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$unique$Animal_ID$Violation,
    c(
      FALSE,
      TRUE,
      TRUE,
      FALSE,
      FALSE
    )
  )
})


test_that("missing values do not count as uniqueness violations", {

  dat <- data.frame(
    ID = c(
      NA,
      NA,
      "A",
      "B"
    )
  )

  result <- apply_audit_rules(
    dat,
    audit_rules(
      unique = "ID"
    )
  )

  expect_false(
    any(
      result$unique$ID$Violation[
        1:2
      ]
    )
  )
})


test_that("required rule detects missing values", {

  rules <- audit_rules(
    required = "Animal_ID"
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$required$Animal_ID$Violation,
    c(
      FALSE,
      FALSE,
      FALSE,
      TRUE,
      FALSE
    )
  )
})


test_that("required rule detects blank strings", {

  rules <- audit_rules(
    required = "Species"
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$required$Species$Violation,
    c(
      FALSE,
      FALSE,
      FALSE,
      TRUE,
      FALSE
    )
  )
})


test_that("required rule works with numeric variables", {

  rules <- audit_rules(
    required = "Age"
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$required$Age$Violation,
    c(
      FALSE,
      FALSE,
      FALSE,
      TRUE,
      FALSE
    )
  )
})


test_that("pattern rules detect violations", {

  rules <- audit_rules(
    pattern = list(
      Animal_ID =
        "^A[0-9]{3}$"
    )
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$pattern$Animal_ID$Violation,
    c(
      FALSE,
      FALSE,
      FALSE,
      FALSE,
      TRUE
    )
  )
})


test_that("missing values are not pattern violations", {

  dat <- data.frame(
    ID = c(
      "A001",
      NA,
      "BAD"
    )
  )

  result <- apply_audit_rules(
    dat,
    audit_rules(
      pattern = list(
        ID = "^A[0-9]{3}$"
      )
    )
  )

  expect_equal(
    result$pattern$ID$Violation,
    c(
      FALSE,
      FALSE,
      TRUE
    )
  )
})


test_that("multiple rule types are combined", {

  rules <- audit_rules(
    range = list(
      Age = c(0, 30)
    ),
    category = list(
      Species = c(
        "Dog",
        "Cat",
        "Cattle"
      )
    ),
    unique = "Animal_ID",
    required = c(
      "Animal_ID",
      "Species"
    ),
    pattern = list(
      Animal_ID =
        "^A[0-9]{3}$"
    )
  )

  result <- apply_audit_rules(
    make_rule_data(),
    rules
  )

  expect_equal(
    result$status,
    "Violations detected"
  )

  expect_true(
    result$total_violations > 0L
  )

  expect_equal(
    result$total_violations,
    sum(result$summary$Violations)
  )
})


test_that("summary contains all rule types", {

  result <- apply_audit_rules(
    make_rule_data(),
    audit_rules()
  )

  expect_equal(
    result$summary$Rule,
    c(
      "Range",
      "Category",
      "Unique",
      "Required",
      "Pattern"
    )
  )
})


test_that("rule referencing absent variable produces error", {

  rules <- audit_rules(
    range = list(
      Weight = c(0, 100)
    )
  )

  expect_error(
    apply_audit_rules(
      make_rule_data(),
      rules
    ),
    "Variable\\(s\\) referenced by rules not found in 'data': Weight"
  )
})


test_that("multiple missing rule variables are reported", {

  rules <- audit_rules(
    range = list(
      Weight = c(0, 100)
    ),
    required = "Breed"
  )

  expect_error(
    apply_audit_rules(
      make_rule_data(),
      rules
    ),
    "Weight, Breed"
  )
})


test_that("non DataAuditRules object is rejected", {

  expect_error(
    apply_audit_rules(
      make_rule_data(),
      list()
    ),
    "'rules' must be an object created by audit_rules"
  )
})


test_that("malformed DataAuditRules object is rejected", {

  rules <- list(
    range = NULL
  )

  class(rules) <- c(
    "DataAuditRules",
    "list"
  )

  expect_error(
    apply_audit_rules(
      make_rule_data(),
      rules
    ),
    "Invalid 'DataAuditRules' object"
  )
})


test_that("invalid data is rejected", {

  rules <- audit_rules()

  expect_error(
    apply_audit_rules(
      1:5,
      rules
    )
  )

  expect_error(
    apply_audit_rules(
      list(x = 1:5),
      rules
    )
  )
})


test_that("range output contains limits", {

  result <- apply_audit_rules(
    make_rule_data(),
    audit_rules(
      range = list(
        Age = c(0, 30)
      )
    )
  )

  expect_true(
    all(
      result$range$Age$Minimum == 0
    )
  )

  expect_true(
    all(
      result$range$Age$Maximum == 30
    )
  )
})


test_that("pattern output contains pattern", {

  result <- apply_audit_rules(
    make_rule_data(),
    audit_rules(
      pattern = list(
        Animal_ID =
          "^A[0-9]{3}$"
      )
    )
  )

  expect_true(
    all(
      result$pattern$Animal_ID$Pattern ==
        "^A[0-9]{3}$"
    )
  )
})
