defmodule Towerops.Devices.MikrotikBackupsTest do
  use Towerops.DataCase, async: true

  import Towerops.AccountsFixtures

  alias Towerops.Devices
  alias Towerops.Devices.MikrotikBackups
  alias Towerops.Organizations
  alias Towerops.Sites

  describe "compress_config/1" do
    test "compresses config text with gzip" do
      # Use larger config for better compression ratio
      config = String.duplicate("# Test config\n/system identity\nset name=Router1\n", 20)

      compressed = MikrotikBackups.compress_config(config)

      # Compressed should be binary and smaller than original
      assert is_binary(compressed)
      assert byte_size(compressed) < byte_size(config)
    end

    test "roundtrip compress and decompress preserves config" do
      config = String.duplicate("# Test config\n/system identity\nset name=Router1\n", 10)

      compressed = MikrotikBackups.compress_config(config)
      decompressed = MikrotikBackups.decompress_config(compressed)

      assert decompressed == config
    end
  end

  describe "normalize_config/1" do
    test "removes timestamp comments from RouterOS config" do
      config = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      # another comment
      /interface ethernet
      """

      normalized = MikrotikBackups.normalize_config(config)

      # Should have no comment lines
      refute String.contains?(normalized, "#")
      assert String.contains?(normalized, "/system identity")
      assert String.contains?(normalized, "set name=Router1")
    end

    test "trims whitespace and removes empty lines" do
      config = """
        /system identity

      set name=Router1


      /interface ethernet
      """

      normalized = MikrotikBackups.normalize_config(config)

      lines = String.split(normalized, "\n", trim: true)
      # Should be 3 lines with no whitespace
      assert length(lines) == 3
      assert Enum.all?(lines, &(String.trim(&1) == &1))
    end
  end

  describe "calculate_hash/1 and calculate_hash_normalized/1" do
    test "calculates SHA256 hash of raw config" do
      config = "/system identity\nset name=Router1"

      hash = MikrotikBackups.calculate_hash(config)

      # Should be 64-character hex string (SHA256)
      assert String.length(hash) == 64
      assert String.match?(hash, ~r/^[a-f0-9]+$/)
    end

    test "normalized hash is different from raw hash when comments present" do
      config = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      raw_hash = MikrotikBackups.calculate_hash(config)
      normalized_hash = MikrotikBackups.calculate_hash_normalized(config)

      assert raw_hash != normalized_hash
    end

    test "normalized hash ignores timestamp changes" do
      config1 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      config2 = """
      # feb/15/2025 14:22:10 by RouterOS 7.14.1
      /system identity
      set name=Router1
      """

      hash1 = MikrotikBackups.calculate_hash_normalized(config1)
      hash2 = MikrotikBackups.calculate_hash_normalized(config2)

      # Normalized hashes should be identical (same actual config)
      assert hash1 == hash2
    end

    test "normalized hash detects real config changes" do
      config1 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      config2 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router2
      """

      hash1 = MikrotikBackups.calculate_hash_normalized(config1)
      hash2 = MikrotikBackups.calculate_hash_normalized(config2)

      # Different configs should have different hashes
      assert hash1 != hash2
    end
  end

  describe "create_backup/3" do
    test "creates backup with compressed config" do
      device = insert(:device)
      config = "/system identity\nset name=Router1\n/interface ethernet\nset name=ether1"

      assert {:ok, backup} = MikrotikBackups.create_backup(device.id, config, "daily_cron")

      assert backup.device_id == device.id
      assert backup.config_compressed
      assert backup.config_hash
      assert backup.config_hash_normalized
      assert backup.config_size_bytes == byte_size(config)
      assert backup.compressed_size_bytes < backup.config_size_bytes
      assert backup.trigger_source == "daily_cron"
      assert backup.backed_up_at
    end

    test "stores both raw and normalized hashes" do
      device = insert(:device)

      config = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      assert {:ok, backup} = MikrotikBackups.create_backup(device.id, config, "manual")

      # Raw hash should match config with comments
      assert backup.config_hash == MikrotikBackups.calculate_hash(config)
      # Normalized hash should match config without comments
      assert backup.config_hash_normalized == MikrotikBackups.calculate_hash_normalized(config)
    end
  end

  describe "get_latest_backup/1" do
    test "returns most recent backup for device" do
      device = insert(:device)

      _backup1 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-01 10:00:00Z])
      _backup2 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-02 10:00:00Z])
      backup3 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-03 10:00:00Z])

      latest = MikrotikBackups.get_latest_backup(device.id)

      assert latest.id == backup3.id
    end

    test "returns nil when no backups exist" do
      device = insert(:device)

      assert MikrotikBackups.get_latest_backup(device.id) == nil
    end

    test "only returns backups for specified device" do
      device1 = insert(:device)
      device2 = insert(:device)

      backup1 = insert(:mikrotik_backup, device_id: device1.id, backed_up_at: ~U[2025-01-01 10:00:00Z])
      _backup2 = insert(:mikrotik_backup, device_id: device2.id, backed_up_at: ~U[2025-01-02 10:00:00Z])

      latest = MikrotikBackups.get_latest_backup(device1.id)

      assert latest.id == backup1.id
    end
  end

  describe "needs_backup?/2" do
    test "returns true when no previous backup exists" do
      device = insert(:device)
      config = "/system identity\nset name=Router1"

      assert {:ok, true} = MikrotikBackups.needs_backup?(device.id, config)
    end

    test "returns false when config hasn't changed (using normalized hash)" do
      device = insert(:device)

      config1 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      config2 = """
      # feb/15/2025 14:22:10 by RouterOS 7.14.1
      /system identity
      set name=Router1
      """

      # Create initial backup
      {:ok, _backup} = MikrotikBackups.create_backup(device.id, config1, "daily_cron")

      # Second config with different timestamp should not need backup
      assert {:ok, false} = MikrotikBackups.needs_backup?(device.id, config2)
    end

    test "returns true when config has actually changed" do
      device = insert(:device)

      config1 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router1
      """

      config2 = """
      # jan/02/1970 00:38:40 by RouterOS 7.14
      /system identity
      set name=Router2
      """

      # Create initial backup
      {:ok, _backup} = MikrotikBackups.create_backup(device.id, config1, "daily_cron")

      # Second config with actual change should need backup
      assert {:ok, true} = MikrotikBackups.needs_backup?(device.id, config2)
    end
  end

  describe "list_device_backups/2" do
    test "returns backups ordered by most recent first" do
      device = insert(:device)

      backup1 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-01 10:00:00Z])
      backup2 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-03 10:00:00Z])
      backup3 = insert(:mikrotik_backup, device_id: device.id, backed_up_at: ~U[2025-01-02 10:00:00Z])

      backups = MikrotikBackups.list_device_backups(device.id)

      assert length(backups) == 3
      assert hd(backups).id == backup2.id
      assert Enum.at(backups, 1).id == backup3.id
      assert Enum.at(backups, 2).id == backup1.id
    end

    test "limits results when limit provided" do
      device = insert(:device)

      for i <- 1..10 do
        insert(:mikrotik_backup, device_id: device.id, backed_up_at: DateTime.add(~U[2025-01-01 10:00:00Z], i, :day))
      end

      backups = MikrotikBackups.list_device_backups(device.id, limit: 5)

      assert length(backups) == 5
    end

    test "only returns backups for specified device" do
      device1 = insert(:device)
      device2 = insert(:device)

      insert(:mikrotik_backup, device_id: device1.id)
      insert(:mikrotik_backup, device_id: device2.id)
      insert(:mikrotik_backup, device_id: device2.id)

      backups = MikrotikBackups.list_device_backups(device1.id)

      assert length(backups) == 1
      assert hd(backups).device_id == device1.id
    end
  end

  describe "get_backup!/1" do
    test "returns backup by id" do
      device = insert(:device)
      backup = insert(:mikrotik_backup, device_id: device.id)

      fetched = MikrotikBackups.get_backup!(backup.id)

      assert fetched.id == backup.id
    end

    test "raises when backup not found" do
      assert_raise Ecto.NoResultsError, fn ->
        MikrotikBackups.get_backup!(Ecto.UUID.generate())
      end
    end
  end

  # Helper functions

  defp insert(:device) do
    user = user_fixture()
    {:ok, organization} = Organizations.create_organization(%{name: "Test Org"}, user.id)
    {:ok, site} = Sites.create_site(%{name: "Test Site", organization_id: organization.id})

    {:ok, device} =
      Devices.create_device(
        %{
          name: "Test Device",
          ip_address: "192.168.1.1",
          site_id: site.id,
          organization_id: organization.id,
          monitoring_enabled: false,
          snmp_enabled: false
        },
        bypass_limits: true
      )

    device
  end

  defp insert(:mikrotik_backup, attrs) do
    config = attrs[:config] || "/system identity\nset name=Router1"

    {:ok, backup} =
      MikrotikBackups.create_backup(
        attrs[:device_id],
        config,
        attrs[:trigger_source] || "daily_cron"
      )

    # Update backed_up_at if provided
    backup =
      if attrs[:backed_up_at] do
        backup
        |> Ecto.Changeset.change(backed_up_at: attrs[:backed_up_at])
        |> Repo.update!()
      else
        backup
      end

    backup
  end
end
