package code.api.util.http4s

import code.api.util.APIUtil.ResourceDoc
import code.api.util.ApiTag.ResourceDocTag
import com.openbankproject.commons.util.ApiVersion
import net.liftweb.json.JsonAST.JObject
import org.http4s._
import org.scalatest.{FeatureSpec, GivenWhenThen, Matchers, Tag}

import scala.collection.mutable.ArrayBuffer

/**
 * Unit tests for ResourceDocMatcher
 * 
 * Tests ResourceDoc matching and path parameter extraction:
 * - Matching by verb and exact path
 * - Matching with BANK_ID variable
 * - Matching with BANK_ID + ACCOUNT_ID variables
 * - Matching with BANK_ID + ACCOUNT_ID + VIEW_ID variables
 * - Matching with COUNTERPARTY_ID variable
 * - Non-matching requests return None
 * - Path parameter extraction for all variable types
 * 
 */
class ResourceDocMatcherTest extends FeatureSpec with Matchers with GivenWhenThen {
  
  object ResourceDocMatcherTag extends Tag("ResourceDocMatcher")
  
  // Helper to create minimal ResourceDoc for testing
  private def createResourceDoc(
    verb: String,
    url: String,
    operationId: String = "testOperation"
  ): ResourceDoc = {
    ResourceDoc(
      partialFunction = null, // Not needed for matching tests
      implementedInApiVersion = ApiVersion.v7_0_0,
      partialFunctionName = operationId,
      requestVerb = verb,
      requestUrl = url,
      summary = "Test endpoint",
      description = "Test description",
      exampleRequestBody = JObject(Nil),
      successResponseBody = JObject(Nil),
      errorResponseBodies = List.empty,
      tags = List(ResourceDocTag("test")),
      roles = None
    )
  }
  
  feature("ResourceDocMatcher - Exact path matching") {
    
    scenario("Match GET request with exact path", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBanks")
    }
    
    scenario("Match POST request with exact path", ResourceDocMatcherTag) {
      Given("A ResourceDoc for POST /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("POST", "/banks", "createBank")
      )
      
      When("Matching a POST request to /obp/v7.0.0/banks")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("createBank")
    }
    
    scenario("Match request with multi-segment path", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /management/metrics")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/management/metrics", "getMetrics")
      )
      
      When("Matching a GET request to /obp/v7.0.0/management/metrics")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/management/metrics")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getMetrics")
    }
    
    scenario("Verb mismatch returns None", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching a POST request to /obp/v7.0.0/banks")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("POST", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
    
    scenario("Path mismatch returns None", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching a GET request to /obp/v7.0.0/accounts")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
  }
  
  feature("ResourceDocMatcher - BANK_ID variable matching") {
    
    scenario("Match request with BANK_ID variable", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID", "getBank")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBank")
    }
    
    scenario("Match request with BANK_ID and additional segments", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/test-bank-1/accounts")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank-1/accounts")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBankAccounts")
    }
    
    scenario("Extract BANK_ID parameter value", ResourceDocMatcherTag) {
      Given("A matched ResourceDoc with BANK_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
      
      When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should extract BANK_ID value")
      params should contain key "BANK_ID"
      params("BANK_ID") should equal("gh.29.de")
    }
  }
  
  feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID variables") {
    
    scenario("Match request with BANK_ID and ACCOUNT_ID variables", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de/accounts/test1")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBankAccount")
    }
    
    scenario("Extract BANK_ID and ACCOUNT_ID parameter values", ResourceDocMatcherTag) {
      Given("A matched ResourceDoc with BANK_ID and ACCOUNT_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID", "getBankAccount")
      
      When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de/accounts/test1")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should extract both BANK_ID and ACCOUNT_ID values")
      params should contain key "BANK_ID"
      params should contain key "ACCOUNT_ID"
      params("BANK_ID") should equal("gh.29.de")
      params("ACCOUNT_ID") should equal("test1")
    }
    
    scenario("Match request with BANK_ID, ACCOUNT_ID and additional segments", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/transactions")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/transactions", "getTransactions")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/test-bank/accounts/acc-123/transactions")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank/accounts/acc-123/transactions")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getTransactions")
    }
  }
  
  feature("ResourceDocMatcher - BANK_ID + ACCOUNT_ID + VIEW_ID variables") {
    
    scenario("Match request with BANK_ID, ACCOUNT_ID and VIEW_ID variables", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getTransactionsForView")
    }
    
    scenario("Extract BANK_ID, ACCOUNT_ID and VIEW_ID parameter values", ResourceDocMatcherTag) {
      Given("A matched ResourceDoc with BANK_ID, ACCOUNT_ID and VIEW_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions", "getTransactionsForView")
      
      When("Extracting path parameters from /obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/transactions")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should extract all three parameter values")
      params should contain key "BANK_ID"
      params should contain key "ACCOUNT_ID"
      params should contain key "VIEW_ID"
      params("BANK_ID") should equal("gh.29.de")
      params("ACCOUNT_ID") should equal("test1")
      params("VIEW_ID") should equal("owner")
    }
    
    scenario("Match request with VIEW_ID in different position", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/account", "getAccountForView")
      )
      
      When("Matching a GET request to /obp/v7.0.0/banks/test-bank/accounts/acc-1/public/account")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/test-bank/accounts/acc-1/public/account")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getAccountForView")
    }
  }
  
  feature("ResourceDocMatcher - COUNTERPARTY_ID variable") {
    
    scenario("Match request with COUNTERPARTY_ID variable", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty")
      )
      
      When("Matching a GET request with counterparty ID")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getCounterparty")
    }
    
    scenario("Extract COUNTERPARTY_ID parameter value", ResourceDocMatcherTag) {
      Given("A matched ResourceDoc with COUNTERPARTY_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/counterparties/COUNTERPARTY_ID", "getCounterparty")
      
      When("Extracting path parameters")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts/test1/owner/counterparties/ff010868-ac7d-4f96-9fc5-70dd5757e891")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should extract all parameter values including COUNTERPARTY_ID")
      params should contain key "BANK_ID"
      params should contain key "ACCOUNT_ID"
      params should contain key "VIEW_ID"
      params should contain key "COUNTERPARTY_ID"
      params("BANK_ID") should equal("gh.29.de")
      params("ACCOUNT_ID") should equal("test1")
      params("VIEW_ID") should equal("owner")
      params("COUNTERPARTY_ID") should equal("ff010868-ac7d-4f96-9fc5-70dd5757e891")
    }
    
    scenario("Match request with COUNTERPARTY_ID in different URL structure", ResourceDocMatcherTag) {
      Given("A ResourceDoc for DELETE /management/counterparties/COUNTERPARTY_ID")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("DELETE", "/management/counterparties/COUNTERPARTY_ID", "deleteCounterparty")
      )
      
      When("Matching a DELETE request")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/management/counterparties/counterparty-123")
      val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("deleteCounterparty")
    }
  }
  
  feature("ResourceDocMatcher - Non-matching requests") {
    
    scenario("Return None when no ResourceDoc matches", ResourceDocMatcherTag) {
      Given("ResourceDocs for specific endpoints")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks"),
        createResourceDoc("GET", "/banks/BANK_ID", "getBank"),
        createResourceDoc("POST", "/banks", "createBank")
      )
      
      When("Matching a request that doesn't match any ResourceDoc")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
    
    scenario("Return None when verb doesn't match", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching a DELETE request to /obp/v7.0.0/banks")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("DELETE", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
    
    scenario("Return None when path segment count doesn't match", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
      )
      
      When("Matching a request with different segment count")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
    
    scenario("Return None when literal segments don't match", ResourceDocMatcherTag) {
      Given("A ResourceDoc for GET /banks/BANK_ID/accounts")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts")
      )
      
      When("Matching a request with different literal segment")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/transactions")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should return None")
      result should be(None)
    }
  }
  
  feature("ResourceDocMatcher - Path parameter extraction edge cases") {
    
    scenario("Extract parameters from path with no variables", ResourceDocMatcherTag) {
      Given("A ResourceDoc with no path variables")
      val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
      
      When("Extracting path parameters")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should return empty map")
      params should be(empty)
    }
    
    scenario("Extract parameters with special characters in values", ResourceDocMatcherTag) {
      Given("A ResourceDoc with BANK_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
      
      When("Extracting path parameters with special characters")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de-test_bank")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should extract the full value including special characters")
      params should contain key "BANK_ID"
      params("BANK_ID") should equal("gh.29.de-test_bank")
    }
    
    scenario("Return empty map when path doesn't match template", ResourceDocMatcherTag) {
      Given("A ResourceDoc for /banks/BANK_ID")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
      
      When("Extracting parameters from path with different segment count")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/accounts")
      val params = ResourceDocMatcher.extractPathParams(path, resourceDoc)
      
      Then("Should return empty map due to segment count mismatch")
      params should be(empty)
    }
  }
  
  feature("ResourceDocMatcher - attachToCallContext") {
    
    scenario("Attach ResourceDoc to CallContext", ResourceDocMatcherTag) {
      Given("A CallContext and a matched ResourceDoc")
      val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
      val callContext = code.api.util.CallContext(
        correlationId = "test-correlation-id"
      )
      
      When("Attaching ResourceDoc to CallContext")
      val updatedContext = ResourceDocMatcher.attachToCallContext(callContext, resourceDoc)
      
      Then("CallContext should have resourceDocument set")
      updatedContext.resourceDocument should be(defined)
      updatedContext.resourceDocument.get should equal(resourceDoc)
    }
    
    scenario("Attach ResourceDoc sets operationId", ResourceDocMatcherTag) {
      Given("A CallContext and a matched ResourceDoc")
      val resourceDoc = createResourceDoc("GET", "/banks/BANK_ID", "getBank")
      val callContext = code.api.util.CallContext(
        correlationId = "test-correlation-id"
      )
      
      When("Attaching ResourceDoc to CallContext")
      val updatedContext = ResourceDocMatcher.attachToCallContext(callContext, resourceDoc)
      
      Then("CallContext should have operationId set")
      updatedContext.operationId should be(defined)
      updatedContext.operationId.get should equal(resourceDoc.operationId)
    }
    
    scenario("Preserve other CallContext fields when attaching ResourceDoc", ResourceDocMatcherTag) {
      Given("A CallContext with existing fields")
      val resourceDoc = createResourceDoc("GET", "/banks", "getBanks")
      val originalContext = code.api.util.CallContext(
        correlationId = "test-correlation-id",
        url = "/obp/v7.0.0/banks",
        verb = "GET",
        implementedInVersion = "v7.0.0"
      )
      
      When("Attaching ResourceDoc to CallContext")
      val updatedContext = ResourceDocMatcher.attachToCallContext(originalContext, resourceDoc)
      
      Then("Other fields should be preserved")
      updatedContext.correlationId should equal(originalContext.correlationId)
      updatedContext.url should equal(originalContext.url)
      updatedContext.verb should equal(originalContext.verb)
      updatedContext.implementedInVersion should equal(originalContext.implementedInVersion)
    }
  }
  
  feature("ResourceDocMatcher - Multiple ResourceDocs selection") {
    
    scenario("Select correct ResourceDoc from multiple candidates", ResourceDocMatcherTag) {
      Given("Multiple ResourceDocs with different paths")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks"),
        createResourceDoc("GET", "/banks/BANK_ID", "getBank"),
        createResourceDoc("GET", "/banks/BANK_ID/accounts", "getBankAccounts"),
        createResourceDoc("POST", "/banks", "createBank")
      )
      
      When("Matching a specific request")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks/gh.29.de/accounts")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should select the most specific matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBankAccounts")
    }
    
    scenario("Match first ResourceDoc when multiple exact matches exist", ResourceDocMatcherTag) {
      Given("Multiple ResourceDocs with same path and verb")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks1"),
        createResourceDoc("GET", "/banks", "getBanks2")
      )
      
      When("Matching a request")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should return the first matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBanks1")
    }
  }
  
  feature("ResourceDocMatcher - Case sensitivity") {
    
    scenario("HTTP verb matching is case-insensitive", ResourceDocMatcherTag) {
      Given("A ResourceDoc with uppercase GET")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching with lowercase get")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/banks")
      val result = ResourceDocMatcher.findResourceDoc("get", path, resourceDocs)
      
      Then("Should find the matching ResourceDoc")
      result should be(defined)
      result.get.partialFunctionName should equal("getBanks")
    }
    
    scenario("Path matching is case-sensitive for literal segments", ResourceDocMatcherTag) {
      Given("A ResourceDoc for /banks")
      val resourceDocs = ArrayBuffer(
        createResourceDoc("GET", "/banks", "getBanks")
      )
      
      When("Matching with different case /Banks")
      val path = Uri.Path.unsafeFromString("/obp/v7.0.0/Banks")
      val result = ResourceDocMatcher.findResourceDoc("GET", path, resourceDocs)
      
      Then("Should not match (case-sensitive)")
      result should be(None)
    }
  }
}
